Merge changes If2d46362,I2472414b into androidx-main am: 85ea688ec5

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2053546

Change-Id: I12e916314cba242de0e67de33cce72b85c151557
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index e01b95c..176077e 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -65,6 +65,7 @@
     method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -163,6 +164,21 @@
     method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
   }
 
+  public final class PickVisualMediaRequest {
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+  }
+
+  public static final class PickVisualMediaRequest.Builder {
+    ctor public PickVisualMediaRequest.Builder();
+    method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+  public final class PickVisualMediaRequestKt {
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
 }
 
 package androidx.activity.result.contract {
@@ -239,6 +255,49 @@
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
+  @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+    ctor public ActivityResultContracts.PickVisualMedia();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public static final boolean isPhotoPickerAvailable();
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.Companion {
+    method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public boolean isPhotoPickerAvailable();
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+    method public String getMimeType();
+    property public final String mimeType;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+  }
+
+  public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+  }
+
   public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
     ctor public ActivityResultContracts.RequestMultiplePermissions();
     method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index e01b95c..176077e 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -65,6 +65,7 @@
     method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -163,6 +164,21 @@
     method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
   }
 
+  public final class PickVisualMediaRequest {
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+  }
+
+  public static final class PickVisualMediaRequest.Builder {
+    ctor public PickVisualMediaRequest.Builder();
+    method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+  public final class PickVisualMediaRequestKt {
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
 }
 
 package androidx.activity.result.contract {
@@ -239,6 +255,49 @@
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
+  @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+    ctor public ActivityResultContracts.PickVisualMedia();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public static final boolean isPhotoPickerAvailable();
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.Companion {
+    method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public boolean isPhotoPickerAvailable();
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+    method public String getMimeType();
+    property public final String mimeType;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+  }
+
+  public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+  }
+
   public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
     ctor public ActivityResultContracts.RequestMultiplePermissions();
     method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 5fdb966..0a8d8b9 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -64,6 +64,7 @@
     method @MainThread public void addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
     method @MainThread public boolean hasEnabledCallbacks();
     method @MainThread public void onBackPressed();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void setOnBackInvokedDispatcher(android.window.OnBackInvokedDispatcher);
   }
 
   public interface OnBackPressedDispatcherOwner extends androidx.lifecycle.LifecycleOwner {
@@ -162,6 +163,21 @@
     method public androidx.activity.result.IntentSenderRequest.Builder setFlags(int, int);
   }
 
+  public final class PickVisualMediaRequest {
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
+  }
+
+  public static final class PickVisualMediaRequest.Builder {
+    ctor public PickVisualMediaRequest.Builder();
+    method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
+  public final class PickVisualMediaRequestKt {
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+  }
+
 }
 
 package androidx.activity.result.contract {
@@ -238,6 +254,49 @@
     method public android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
   }
 
+  @RequiresApi(19) public static class ActivityResultContracts.PickMultipleVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,java.util.List<android.net.Uri>> {
+    ctor public ActivityResultContracts.PickMultipleVisualMedia(optional int maxItems);
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.util.List<android.net.Uri>>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final java.util.List<android.net.Uri> parseResult(int resultCode, android.content.Intent? intent);
+  }
+
+  public static class ActivityResultContracts.PickVisualMedia extends androidx.activity.result.contract.ActivityResultContract<androidx.activity.result.PickVisualMediaRequest,android.net.Uri> {
+    ctor public ActivityResultContracts.PickVisualMedia();
+    method @CallSuper public android.content.Intent createIntent(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public final androidx.activity.result.contract.ActivityResultContract.SynchronousResult<android.net.Uri>? getSynchronousResult(android.content.Context context, androidx.activity.result.PickVisualMediaRequest input);
+    method public static final String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public static final boolean isPhotoPickerAvailable();
+    method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.Companion {
+    method public String? getVisualMimeType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType input);
+    method public boolean isPhotoPickerAvailable();
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageAndVideo implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.ImageOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.SingleMimeType implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    ctor public ActivityResultContracts.PickVisualMedia.SingleMimeType(String mimeType);
+    method public String getMimeType();
+    property public final String mimeType;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.VideoOnly implements androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType {
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VideoOnly INSTANCE;
+  }
+
+  public static sealed interface ActivityResultContracts.PickVisualMedia.VisualMediaType {
+  }
+
   public static final class ActivityResultContracts.RequestMultiplePermissions extends androidx.activity.result.contract.ActivityResultContract<java.lang.String[],java.util.Map<java.lang.String,java.lang.Boolean>> {
     ctor public ActivityResultContracts.RequestMultiplePermissions();
     method public android.content.Intent createIntent(android.content.Context context, String![] input);
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index 3e7fefd..389c8bc 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -115,6 +115,7 @@
             finish()
         }
     }
+    @Suppress("DEPRECATION")
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         launcher.launch(intent.getParcelableExtra("destinationIntent"))
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
new file mode 100644
index 0000000..5e1790d
--- /dev/null
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.activity
+
+import android.os.Build
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S_V2)
+class OnBackPressedDispatcherInvokerTest {
+
+    @Test
+    fun testSimpleInvoker() {
+        var registerCount = 0
+        var unregisterCount = 0
+        val invoker = object : OnBackInvokedDispatcher {
+            override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+                registerCount++
+            }
+
+            override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+                unregisterCount++
+            }
+        }
+
+        val dispatcher = OnBackPressedDispatcher()
+
+        dispatcher.setOnBackInvokedDispatcher(invoker)
+
+        val callback = object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() { }
+        }
+
+        dispatcher.addCallback(callback)
+
+        assertThat(registerCount).isEqualTo(1)
+
+        callback.remove()
+
+        assertThat(unregisterCount).isEqualTo(1)
+    }
+
+    @Test
+    fun testInvokerEnableDisable() {
+        var registerCount = 0
+        var unregisterCount = 0
+        val invoker = object : OnBackInvokedDispatcher {
+            override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+                registerCount++
+            }
+
+            override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+                unregisterCount++
+            }
+        }
+
+        val dispatcher = OnBackPressedDispatcher()
+
+        dispatcher.setOnBackInvokedDispatcher(invoker)
+
+        val callback = object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() { }
+        }
+
+        dispatcher.addCallback(callback)
+
+        assertThat(registerCount).isEqualTo(1)
+
+        callback.isEnabled = false
+
+        assertThat(unregisterCount).isEqualTo(1)
+
+        callback.isEnabled = true
+
+        assertThat(registerCount).isEqualTo(2)
+    }
+
+    @Test
+    fun testCallbackEnabledDisabled() {
+        val callback = object : OnBackPressedCallback(false) {
+            override fun handleOnBackPressed() {
+                TODO("Not yet implemented")
+            }
+        }
+
+        callback.isEnabled = true
+        callback.isEnabled = false
+    }
+
+    @Test
+    fun testInvokerAddDisabledCallback() {
+        var registerCount = 0
+        var unregisterCount = 0
+        val invoker = object : OnBackInvokedDispatcher {
+            override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+                registerCount++
+            }
+
+            override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+                unregisterCount++
+            }
+        }
+
+        val callback = object : OnBackPressedCallback(false) {
+            override fun handleOnBackPressed() { }
+        }
+
+        val dispatcher = OnBackPressedDispatcher()
+
+        dispatcher.setOnBackInvokedDispatcher(invoker)
+
+        dispatcher.addCallback(callback)
+
+        assertThat(registerCount).isEqualTo(0)
+
+        callback.isEnabled = true
+
+        assertThat(registerCount).isEqualTo(1)
+
+        callback.isEnabled = false
+
+        assertThat(unregisterCount).isEqualTo(1)
+    }
+
+    @Test
+    fun testInvokerAddEnabledCallbackBeforeSet() {
+        var registerCount = 0
+        var unregisterCount = 0
+        val invoker = object : OnBackInvokedDispatcher {
+            override fun registerOnBackInvokedCallback(p0: Int, p1: OnBackInvokedCallback) {
+                registerCount++
+            }
+
+            override fun unregisterOnBackInvokedCallback(p0: OnBackInvokedCallback) {
+                unregisterCount++
+            }
+        }
+
+        val callback = object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() { }
+        }
+
+        val dispatcher = OnBackPressedDispatcher()
+        dispatcher.addCallback(callback)
+
+        dispatcher.setOnBackInvokedDispatcher(invoker)
+
+        assertThat(registerCount).isEqualTo(1)
+
+        callback.isEnabled = false
+
+        assertThat(unregisterCount).isEqualTo(1)
+    }
+}
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 3f531b3..37422c4 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -65,6 +65,7 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.core.app.ActivityCompat;
 import androidx.core.app.ActivityOptionsCompat;
@@ -76,6 +77,7 @@
 import androidx.core.content.ContextCompat;
 import androidx.core.content.OnConfigurationChangedProvider;
 import androidx.core.content.OnTrimMemoryProvider;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Consumer;
 import androidx.core.view.MenuHost;
 import androidx.core.view.MenuHostHelper;
@@ -150,8 +152,8 @@
                 @Override
                 public void run() {
                     // Calling onBackPressed() on an Activity with its state saved can cause an
-                    // error on devices on API levels before 26. We catch that specific error and
-                    // throw all others.
+                    // error on devices on API levels before 26. We catch that specific error
+                    // and throw all others.
                     try {
                         ComponentActivity.super.onBackPressed();
                     } catch (IllegalStateException e) {
@@ -170,6 +172,7 @@
 
     private final ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {
 
+        @SuppressWarnings("deprecation")
         @Override
         public <I, O> void onLaunch(
                 final int requestCode,
@@ -344,6 +347,7 @@
      * If your ComponentActivity is annotated with {@link ContentView}, this will
      * call {@link #setContentView(int)} for you.
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         // Restore the Saved State first so that it is available to
@@ -352,6 +356,9 @@
         mContextAwareHelper.dispatchOnContextAvailable(this);
         super.onCreate(savedInstanceState);
         ReportFragment.injectIfNeededIn(this);
+        if (BuildCompat.isAtLeastT()) {
+            mOnBackPressedDispatcher.setOnBackInvokedDispatcher(getOnBackInvokedDispatcher());
+        }
         if (mContentLayoutId != 0) {
             setContentView(mContentLayoutId);
         }
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
index 5fc26dc..afa44f3 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
@@ -18,6 +18,10 @@
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
 import androidx.lifecycle.LifecycleOwner;
 
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -44,6 +48,7 @@
 
     private boolean mEnabled;
     private CopyOnWriteArrayList<Cancellable> mCancellables = new CopyOnWriteArrayList<>();
+    private Consumer<Boolean> mEnabledConsumer;
 
     /**
      * Create a {@link OnBackPressedCallback}.
@@ -66,9 +71,13 @@
      *
      * @param enabled whether the callback should be considered enabled
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     @MainThread
     public final void setEnabled(boolean enabled) {
         mEnabled = enabled;
+        if (mEnabledConsumer != null) {
+            mEnabledConsumer.accept(mEnabled);
+        }
     }
 
     /**
@@ -106,4 +115,8 @@
     void removeCancellable(@NonNull Cancellable cancellable) {
         mCancellables.remove(cancellable);
     }
+
+    void setIsEnabledConsumer(@Nullable Consumer<Boolean> isEnabled) {
+        mEnabledConsumer = isEnabled;
+    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
index 6ff39f1..e306a9b 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
@@ -17,10 +17,18 @@
 package androidx.activity;
 
 import android.annotation.SuppressLint;
+import android.os.Build;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleEventObserver;
 import androidx.lifecycle.LifecycleOwner;
@@ -59,6 +67,42 @@
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
 
+    private Consumer<Boolean> mEnabledConsumer;
+
+    private OnBackInvokedCallback mOnBackInvokedCallback;
+    private OnBackInvokedDispatcher mInvokedDispatcher;
+    private boolean mBackInvokedCallbackRegistered = false;
+
+    /**
+     * Sets the {@link OnBackInvokedDispatcher} for handling system back for Android SDK T+.
+     *
+     * @param invoker the OnBackInvokedDispatcher to be set on this dispatcher
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public void setOnBackInvokedDispatcher(@NonNull OnBackInvokedDispatcher invoker) {
+        mInvokedDispatcher = invoker;
+        updateBackInvokedCallbackState();
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    void updateBackInvokedCallbackState() {
+        boolean shouldBeRegistered = hasEnabledCallbacks();
+        if (mInvokedDispatcher != null) {
+            if (shouldBeRegistered && !mBackInvokedCallbackRegistered) {
+                Api33Impl.registerOnBackInvokedCallback(
+                        mInvokedDispatcher,
+                        OnBackInvokedDispatcher.PRIORITY_OVERLAY,
+                        mOnBackInvokedCallback
+                );
+                mBackInvokedCallbackRegistered = true;
+            } else if (!shouldBeRegistered && mBackInvokedCallbackRegistered) {
+                Api33Impl.unregisterOnBackInvokedCallback(mInvokedDispatcher,
+                        mOnBackInvokedCallback);
+                mBackInvokedCallbackRegistered = false;
+            }
+        }
+    }
+
     /**
      * Create a new OnBackPressedDispatcher that dispatches System back button pressed events
      * to one or more {@link OnBackPressedCallback} instances.
@@ -74,8 +118,22 @@
      * @param fallbackOnBackPressed The Runnable that should be triggered if
      * {@link #onBackPressed()} is called when {@link #hasEnabledCallbacks()} returns false.
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
         mFallbackOnBackPressed = fallbackOnBackPressed;
+        if (BuildCompat.isAtLeastT()) {
+            mEnabledConsumer = aBoolean -> {
+                if (BuildCompat.isAtLeastT()) {
+                    updateBackInvokedCallbackState();
+                }
+            };
+            mOnBackInvokedCallback = new OnBackInvokedCallback() {
+                @Override
+                public void onBackInvoked() {
+                    onBackPressed();
+                }
+            };
+        }
     }
 
     /**
@@ -107,6 +165,7 @@
      * @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
      * the callback and remove it from the set of OnBackPressedCallbacks.
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     @MainThread
     @NonNull
@@ -114,6 +173,10 @@
         mOnBackPressedCallbacks.add(onBackPressedCallback);
         OnBackPressedCancellable cancellable = new OnBackPressedCancellable(onBackPressedCallback);
         onBackPressedCallback.addCancellable(cancellable);
+        if (BuildCompat.isAtLeastT()) {
+            updateBackInvokedCallbackState();
+            onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
+        }
         return cancellable;
     }
 
@@ -141,6 +204,7 @@
      *
      * @see #onBackPressed()
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     @SuppressLint("LambdaLast")
     @MainThread
     public void addCallback(@NonNull LifecycleOwner owner,
@@ -152,6 +216,10 @@
 
         onBackPressedCallback.addCancellable(
                 new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback));
+        if (BuildCompat.isAtLeastT()) {
+            updateBackInvokedCallbackState();
+            onBackPressedCallback.setIsEnabledConsumer(mEnabledConsumer);
+        }
     }
 
     /**
@@ -204,10 +272,15 @@
             mOnBackPressedCallback = onBackPressedCallback;
         }
 
+        @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
         @Override
         public void cancel() {
             mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
             mOnBackPressedCallback.removeCancellable(this);
+            if (BuildCompat.isAtLeastT()) {
+                mOnBackPressedCallback.setIsEnabledConsumer(null);
+                updateBackInvokedCallbackState();
+            }
         }
     }
 
@@ -251,4 +324,25 @@
             }
         }
     }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    static class Api33Impl {
+        private Api33Impl() { }
+
+        @DoNotInline
+        static void registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher onBackInvokedDispatcher, int priority,
+                OnBackInvokedCallback onBackInvokedCallback
+        ) {
+            onBackInvokedDispatcher.registerOnBackInvokedCallback(priority, onBackInvokedCallback);
+        }
+
+        @DoNotInline
+        static void unregisterOnBackInvokedCallback(
+                OnBackInvokedDispatcher onBackInvokedDispatcher,
+                OnBackInvokedCallback onBackInvokedCallback
+        ) {
+            onBackInvokedDispatcher.unregisterOnBackInvokedCallback(onBackInvokedCallback);
+        }
+    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
new file mode 100644
index 0000000..f9b3bee
--- /dev/null
+++ b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.activity.result
+
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
+
+/**
+ * Creates a request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ *
+ * @param mediaType type to go into the PickVisualMediaRequest
+ *
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+fun PickVisualMediaRequest(
+    mediaType: VisualMediaType = ImageAndVideo
+) = PickVisualMediaRequest.Builder().setMediaType(mediaType).build()
+
+/**
+ * A request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ */
+class PickVisualMediaRequest internal constructor() {
+
+    var mediaType: VisualMediaType = ImageAndVideo
+        private set
+
+    /**
+     * A builder for constructing [PickVisualMediaRequest] instances.
+     */
+    class Builder {
+
+        private var mediaType: VisualMediaType = ImageAndVideo
+
+        /**
+         * Set the media type for the [PickVisualMediaRequest].
+         *
+         * The type is the mime type to filter by, e.g. `PickVisualMedia.ImageOnly`,
+         * `PickVisualMedia.ImageAndVideo`, `PickVisualMedia.SingleMimeType("image/gif")`
+         *
+         * @param mediaType type to go into the PickVisualMediaRequest
+         * @return This builder.
+         */
+        fun setMediaType(mediaType: VisualMediaType): Builder {
+            this.mediaType = mediaType
+            return this
+        }
+
+        /**
+         * Build the PickVisualMediaRequest specified by this builder.
+         *
+         * @return the newly constructed PickVisualMediaRequest.
+         */
+        fun build(): PickVisualMediaRequest = PickVisualMediaRequest(mediaType).apply {
+            this.mediaType = mediaType
+        }
+    }
+}
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index 9af89ed..aba00e1 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -22,11 +22,13 @@
 import android.graphics.Bitmap
 import android.net.Uri
 import android.os.Build
+import android.os.ext.SdkExtensions.getExtensionVersion
 import android.provider.ContactsContract
 import android.provider.DocumentsContract
 import android.provider.MediaStore
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.PickVisualMediaRequest
 import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.Companion.getClipDataUris
 import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion.ACTION_INTENT_SENDER_REQUEST
 import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.Companion.EXTRA_SEND_INTENT_EXCEPTION
@@ -257,6 +259,7 @@
             input: Void?
         ): SynchronousResult<Bitmap?>? = null
 
+        @Suppress("DEPRECATION")
         final override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
             return intent.takeIf { resultCode == Activity.RESULT_OK }?.getParcelableExtra("data")
         }
@@ -317,6 +320,7 @@
             input: Uri
         ): SynchronousResult<Bitmap?>? = null
 
+        @Suppress("DEPRECATION")
         final override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
             return intent.takeIf { resultCode == Activity.RESULT_OK }?.getParcelableExtra("data")
         }
@@ -597,4 +601,155 @@
             return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
         }
     }
+
+    /**
+     * An [ActivityResultContract] to use the photo picker through [MediaStore.ACTION_PICK_IMAGES]
+     * when available, and else rely on ACTION_OPEN_DOCUMENT.
+     *
+     * The input is a [PickVisualMediaRequest].
+     *
+     * The output is a `Uri` when the user has selected a media or `null` when the user hasn't
+     * selected any item. Keep in mind that `Uri` returned by the photo picker isn't writable.
+     *
+     * This can be extended to override [createIntent] if you wish to pass additional
+     * extras to the Intent created by `super.createIntent()`.
+     */
+    open class PickVisualMedia : ActivityResultContract<PickVisualMediaRequest, Uri?>() {
+        companion object {
+            @JvmStatic
+            fun isPhotoPickerAvailable(): Boolean {
+                return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ||
+                    getExtensionVersion(Build.VERSION_CODES.R) >= 2
+            }
+
+            @JvmStatic
+            fun getVisualMimeType(input: VisualMediaType): String? {
+                return when (input) {
+                    is ImageOnly -> "image/*"
+                    is VideoOnly -> "video/*"
+                    is SingleMimeType -> input.mimeType
+                    is ImageAndVideo -> null
+                }
+            }
+        }
+
+        sealed interface VisualMediaType
+        object ImageOnly : VisualMediaType
+        object VideoOnly : VisualMediaType
+        object ImageAndVideo : VisualMediaType
+        class SingleMimeType(val mimeType: String) : VisualMediaType
+
+        @CallSuper
+        override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
+            // Check if Photo Picker is available on the device
+            return if (isPhotoPickerAvailable()) {
+                Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+                    type = getVisualMimeType(input.mediaType)
+                }
+            } else {
+                // For older devices running KitKat and higher and devices running Android 12
+                // and 13 without the SDK extension that includes the Photo Picker, rely on the
+                // ACTION_OPEN_DOCUMENT intent
+                Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+                    type = getVisualMimeType(input.mediaType)
+
+                    if (type == null) {
+                        // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+                        // intent with multiple mime types
+                        type = "*/*"
+                        putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+                    }
+                }
+            }
+        }
+
+        @Suppress("InvalidNullability")
+        final override fun getSynchronousResult(
+            context: Context,
+            input: PickVisualMediaRequest
+        ): SynchronousResult<Uri?>? = null
+
+        final override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
+            return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
+        }
+    }
+
+    /**
+     * An [ActivityResultContract] to use the Photo Picker through [MediaStore.ACTION_PICK_IMAGES]
+     * when available, and else rely on ACTION_OPEN_DOCUMENT.
+     *
+     * The constructor accepts one parameter `maxItems` to limit the number of selectable items when
+     * using the photo picker to return. Keep in mind that this parameter isn't supported on devices
+     * when the photo picker isn't available.
+     *
+     * The input is a [PickVisualMediaRequest].
+     *
+     * The output is a list `Uri` of the selected media. It can be empty if the user hasn't selected
+     * any items. Keep in mind that `Uri` returned by the photo picker aren't writable.
+     *
+     * This can be extended to override [createIntent] if you wish to pass additional
+     * extras to the Intent created by `super.createIntent()`.
+     */
+    @RequiresApi(19)
+    open class PickMultipleVisualMedia(
+        private val maxItems: Int = getMaxItems()
+    ) : ActivityResultContract<PickVisualMediaRequest, List<@JvmSuppressWildcards Uri>>() {
+
+        init {
+            require(maxItems > 1) {
+                "Max items must be higher than 1"
+            }
+        }
+
+        @CallSuper
+        override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
+            // Check to see if the photo picker is available
+            return if (PickVisualMedia.isPhotoPickerAvailable()) {
+                Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+                    type = PickVisualMedia.getVisualMimeType(input.mediaType)
+
+                    require(maxItems <= MediaStore.getPickImagesMaxLimit()) {
+                        "Max items must be less or equals MediaStore.getPickImagesMaxLimit()"
+                    }
+
+                    putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxItems)
+                }
+            } else {
+                // For older devices running KitKat and higher and devices running Android 12
+                // and 13 without the SDK extension that includes the Photo Picker, rely on the
+                // ACTION_OPEN_DOCUMENT intent
+                Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+                    type = PickVisualMedia.getVisualMimeType(input.mediaType)
+                    putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+
+                    if (type == null) {
+                        // ACTION_OPEN_DOCUMENT requires to set this parameter when launching the
+                        // intent with multiple mime types
+                        type = "*/*"
+                        putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+                    }
+                }
+            }
+        }
+
+        @Suppress("InvalidNullability")
+        final override fun getSynchronousResult(
+            context: Context,
+            input: PickVisualMediaRequest
+        ): SynchronousResult<List<@JvmSuppressWildcards Uri>>? = null
+
+        final override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> {
+            return intent.takeIf {
+                resultCode == Activity.RESULT_OK
+            }?.getClipDataUris() ?: emptyList()
+        }
+
+        internal companion object {
+            internal fun getMaxItems() = if (PickVisualMedia.isPhotoPickerAvailable()) {
+                MediaStore.getPickImagesMaxLimit()
+            } else {
+                Integer.MAX_VALUE
+            }
+        }
+    }
 }
diff --git a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
index 11780b3..947be53 100644
--- a/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
+++ b/activity/integration-tests/testapp/src/main/java/androidx/activity/integration/testapp/MainActivity.kt
@@ -36,11 +36,14 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.PickVisualMediaRequest
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts.CaptureVideo
 import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
 import androidx.activity.result.contract.ActivityResultContracts.GetContent
 import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
+import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
 import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
 import androidx.activity.result.contract.ActivityResultContracts.TakePicture
 import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview
@@ -77,6 +80,10 @@
         toast("Got image: $uri")
     }
 
+    lateinit var pickVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
+
+    lateinit var pickMultipleVisualMedia: ActivityResultLauncher<PickVisualMediaRequest>
+
     lateinit var createDocument: ActivityResultLauncher<String>
 
     lateinit var openDocuments: ActivityResultLauncher<Array<String>>
@@ -92,6 +99,17 @@
         super.onCreate(savedInstanceState)
 
         if (android.os.Build.VERSION.SDK_INT >= 19) {
+            pickVisualMedia = registerForActivityResult(PickVisualMedia()) { uri ->
+                toast("Got image: $uri")
+            }
+            pickMultipleVisualMedia =
+                registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
+                    var media = ""
+                    uris.forEach {
+                        media += "uri: $it \n"
+                    }
+                    toast("Got media files: $media")
+                }
             createDocument = registerForActivityResult(CreateDocument("image/png")) { uri ->
                 toast("Created document: $uri")
             }
@@ -124,10 +142,25 @@
                     val uri = FileProvider.getUriForFile(this@MainActivity, packageName, file)
                     captureVideo.launch(uri)
                 }
-                button("Pick an image") {
+                button("Pick an image (w/ GET_CONTENT)") {
                     getContent.launch("image/*")
                 }
                 if (android.os.Build.VERSION.SDK_INT >= 19) {
+                    button("Pick an image (w/ photo picker)") {
+                        pickVisualMedia.launch(
+                            PickVisualMediaRequest(PickVisualMedia.ImageOnly)
+                        )
+                    }
+                    button("Pick a GIF (w/ photo picker)") {
+                        pickVisualMedia.launch(
+                            PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif"))
+                        )
+                    }
+                    button("Pick 5 visual media max (w/ photo picker)") {
+                        pickMultipleVisualMedia.launch(
+                            PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
+                        )
+                    }
                     button("Create document") {
                         createDocument.launch("Temp")
                     }
diff --git a/appcompat/appcompat/api/current.txt b/appcompat/appcompat/api/current.txt
index 6acef29..3fa4361 100644
--- a/appcompat/appcompat/api/current.txt
+++ b/appcompat/appcompat/api/current.txt
@@ -224,6 +224,7 @@
     method public androidx.appcompat.app.ActionBar? getSupportActionBar();
     method public android.content.Intent? getSupportParentActivityIntent();
     method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+    method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
     method public final boolean onMenuItemSelected(int, android.view.MenuItem);
     method protected void onNightModeChanged(int);
     method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -261,6 +262,8 @@
     method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
     method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
     method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+    method public android.content.Context? getContextForDelegate();
     method public static int getDefaultNightMode();
     method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
     method public int getLocalNightMode();
@@ -280,6 +283,7 @@
     method public abstract void onStart();
     method public abstract void onStop();
     method public abstract boolean requestWindowFeature(int);
+    method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
     method public static void setCompatVectorFromResourcesEnabled(boolean);
     method public abstract void setContentView(android.view.View!);
     method public abstract void setContentView(@LayoutRes int);
@@ -339,6 +343,12 @@
     method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
   }
 
+  public final class AppLocalesMetadataHolderService extends android.app.Service {
+    ctor public AppLocalesMetadataHolderService();
+    method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.os.IBinder onBind(android.content.Intent);
+  }
+
 }
 
 package androidx.appcompat.graphics.drawable {
diff --git a/appcompat/appcompat/api/public_plus_experimental_current.txt b/appcompat/appcompat/api/public_plus_experimental_current.txt
index 6acef29..3fa4361 100644
--- a/appcompat/appcompat/api/public_plus_experimental_current.txt
+++ b/appcompat/appcompat/api/public_plus_experimental_current.txt
@@ -224,6 +224,7 @@
     method public androidx.appcompat.app.ActionBar? getSupportActionBar();
     method public android.content.Intent? getSupportParentActivityIntent();
     method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+    method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
     method public final boolean onMenuItemSelected(int, android.view.MenuItem);
     method protected void onNightModeChanged(int);
     method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -261,6 +262,8 @@
     method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
     method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
     method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+    method public android.content.Context? getContextForDelegate();
     method public static int getDefaultNightMode();
     method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
     method public int getLocalNightMode();
@@ -280,6 +283,7 @@
     method public abstract void onStart();
     method public abstract void onStop();
     method public abstract boolean requestWindowFeature(int);
+    method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
     method public static void setCompatVectorFromResourcesEnabled(boolean);
     method public abstract void setContentView(android.view.View!);
     method public abstract void setContentView(@LayoutRes int);
@@ -339,6 +343,12 @@
     method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
   }
 
+  public final class AppLocalesMetadataHolderService extends android.app.Service {
+    ctor public AppLocalesMetadataHolderService();
+    method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.os.IBinder onBind(android.content.Intent);
+  }
+
 }
 
 package androidx.appcompat.graphics.drawable {
diff --git a/appcompat/appcompat/api/restricted_current.txt b/appcompat/appcompat/api/restricted_current.txt
index 6c1a9f8..9a4e193 100644
--- a/appcompat/appcompat/api/restricted_current.txt
+++ b/appcompat/appcompat/api/restricted_current.txt
@@ -245,6 +245,7 @@
     method public androidx.appcompat.app.ActionBar? getSupportActionBar();
     method public android.content.Intent? getSupportParentActivityIntent();
     method public void onCreateSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
+    method protected void onLocalesChanged(androidx.core.os.LocaleListCompat);
     method public final boolean onMenuItemSelected(int, android.view.MenuItem);
     method protected void onNightModeChanged(@androidx.appcompat.app.AppCompatDelegate.NightMode int);
     method public void onPrepareSupportNavigateUpTaskStack(androidx.core.app.TaskStackBuilder);
@@ -282,6 +283,8 @@
     method public static androidx.appcompat.app.AppCompatDelegate create(android.content.Context, android.app.Activity, androidx.appcompat.app.AppCompatCallback?);
     method public abstract android.view.View! createView(android.view.View?, String!, android.content.Context, android.util.AttributeSet);
     method public abstract <T extends android.view.View> T! findViewById(@IdRes int);
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales();
+    method public android.content.Context? getContextForDelegate();
     method @androidx.appcompat.app.AppCompatDelegate.NightMode public static int getDefaultNightMode();
     method public abstract androidx.appcompat.app.ActionBarDrawerToggle.Delegate? getDrawerToggleDelegate();
     method @androidx.appcompat.app.AppCompatDelegate.NightMode public int getLocalNightMode();
@@ -301,6 +304,7 @@
     method public abstract void onStart();
     method public abstract void onStop();
     method public abstract boolean requestWindowFeature(int);
+    method public static void setApplicationLocales(androidx.core.os.LocaleListCompat);
     method public static void setCompatVectorFromResourcesEnabled(boolean);
     method public abstract void setContentView(android.view.View!);
     method public abstract void setContentView(@LayoutRes int);
@@ -363,6 +367,12 @@
     method protected android.view.View? createView(android.content.Context!, String!, android.util.AttributeSet!);
   }
 
+  public final class AppLocalesMetadataHolderService extends android.app.Service {
+    ctor public AppLocalesMetadataHolderService();
+    method public static android.content.pm.ServiceInfo getServiceInfo(android.content.Context) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public android.os.IBinder onBind(android.content.Intent);
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class WindowDecorActionBar extends androidx.appcompat.app.ActionBar implements androidx.appcompat.widget.ActionBarOverlayLayout.ActionBarVisibilityCallback {
     ctor public WindowDecorActionBar(android.app.Activity!, boolean);
     ctor public WindowDecorActionBar(android.app.Dialog!);
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index b5a068e..cede9fd 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -26,6 +26,15 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.AppCompat">
 
+        <service
+            android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+            android:enabled="false"
+            android:exported="false">
+            <meta-data
+                android:name="autoStoreLocales"
+                android:value="false" />
+        </service>
+
         <activity
             android:name="androidx.appcompat.app.AppCompatActivity"/>
 
@@ -324,6 +333,40 @@
 
         <activity
             android:name="androidx.appcompat.widget.TooltipCompatTestActivity" />
+        <activity android:name="androidx.appcompat.app.LocalesUpdateActivity"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesConfigChangesActivity"
+            android:configChanges="locale|layoutDirection"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesCustomApplyOverrideConfigurationActivity" />
+
+        <activity android:name="androidx.appcompat.app.LocalesDialogFragment"/>
+
+        <activity android:name="androidx.appcompat.app.LocalesCustomAttachBaseContextActivity"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesRotateDoesNotRecreateActivity"
+            android:configChanges="orientation|screenSize"/>
+
+        <activity android:name="androidx.appcompat.app.LocalesActivityA"/>
+
+        <activity android:name="androidx.appcompat.app.LocalesActivityB"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesConfigChangesActivityA"
+            android:configChanges="locale|layoutDirection"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesConfigChangesActivityB"
+            android:configChanges="locale|layoutDirection"/>
+
+        <activity
+            android:name="androidx.appcompat.app.LocalesConfigChangesActivityWithoutLayoutDirection"
+            android:configChanges="locale"/>
+
+        <activity android:name="androidx.appcompat.app.LocalesLateOnCreateActivity"/>
 
     </application>
 
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
new file mode 100644
index 0000000..5719451
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+/**
+ * An activity for locales with a unique class name.
+ */
+public class LocalesActivityA extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java
new file mode 100644
index 0000000..b327efb
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityB.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+/**
+ * An activity for locales with a unique class name.
+ */
+public class LocalesActivityB extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt
new file mode 100644
index 0000000..9ae3adda
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesChangeWhenInBackground.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.appcompat.app
+
+import android.content.Intent
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.TestCase.assertNotSame
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+//  setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesChangeWhenInBackground {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setUp() {
+        // Since no locales are applied as of now, current configuration will have system
+        // locales.
+        systemLocales = LocalesUpdateActivity.getConfigLocales(
+            rule.activity.resources.configuration)
+    }
+
+    @Test
+    fun testLocalesChangeWhenInBackground() {
+
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val firstActivity = rule.activity
+        assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+        // Start a new Activity, so that the original Activity goes into the background
+        val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+        val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+        assertConfigurationLocalesEquals(systemLocales, secondActivity)
+
+        // Now change the locales for the foreground activity
+        val recreatedSecond = setLocalesAndWaitForRecreate(
+            secondActivity,
+            CUSTOM_LOCALE_LIST
+        )
+
+        // Now finish the foreground activity and wait until it is destroyed,
+        // allowing the recreated activity to come to the foreground
+        instrumentation.runOnMainSync { recreatedSecond.finish() }
+        waitUntilState(recreatedSecond, Lifecycle.State.DESTROYED)
+
+        // Assert that the recreated Activity becomes resumed
+        instrumentation.waitForIdleSync()
+        assertNotSame(rule.activity, firstActivity)
+        waitUntilState(rule.activity, Lifecycle.State.RESUMED)
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java
new file mode 100644
index 0000000..d7c757e
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivity.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivity extends LocalesUpdateActivity { }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java
new file mode 100644
index 0000000..c6b0d8b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityA.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivityA extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java
new file mode 100644
index 0000000..c59c33b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityB.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes.
+ */
+public class LocalesConfigChangesActivityB extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java
new file mode 100644
index 0000000..77d7bfb
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesActivityWithoutLayoutDirection.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.appcompat.app;
+
+/**
+ * An activity that handles locales configuration changes and does not handle layoutDirection
+ * configChanges.
+ */
+public class LocalesConfigChangesActivityWithoutLayoutDirection extends LocalesUpdateActivity { }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt
new file mode 100644
index 0000000..a4aff79
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesTestCase.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.appcompat.app
+
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocales
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.withActivity
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+//  setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesConfigChangesTestCase() {
+    private lateinit var scenario: ActivityScenario<LocalesConfigChangesActivity>
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setup() {
+        LocalesUtils.initCustomLocaleList()
+        // By default we'll set the apps to use system locales, which allows us to make better
+        // assumptions in the tests below.
+        // Launch the test activity.
+        scenario = ActivityScenario.launch(LocalesConfigChangesActivity::class.java)
+        scenario.onActivity {
+            // Since no locales are applied as of now, current configuration will have system
+            // locales.
+            systemLocales = LocalesUpdateActivity.getConfigLocales(it.resources.configuration)
+            // expected locales is an overlay of custom and system locales.
+            expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+                CUSTOM_LOCALE_LIST, systemLocales)
+        }
+    }
+
+    @Test
+    fun testOnConfigurationChangeCalledWhileStarted() {
+        scenario.moveToState(Lifecycle.State.RESUMED)
+
+        // Set locales to CUSTOM_LOCALE_LIST.
+        scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+        // Assert that the onConfigurationChange was called with a new correct config.
+        scenario.onActivity {
+            val lastConfig = it.lastConfigurationChangeAndClear
+            assertConfigurationLocalesEquals(
+                expectedLocales,
+                lastConfig!!
+            )
+        }
+
+        // Set locales back to system locales.
+        scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+        // Assert that the onConfigurationChange was called with a new correct config.
+        scenario.onActivity {
+            val lastConfig = it.lastConfigurationChangeAndClear
+            assertConfigurationLocalesEquals(
+                systemLocales, lastConfig!!
+            )
+        }
+    }
+
+    @Test
+    fun testOnConfigurationChangeCalledWhileStopped() {
+        scenario.moveToState(Lifecycle.State.RESUMED)
+        scenario.moveToState(Lifecycle.State.CREATED)
+
+        // Set locales to CUSTOM_LOCALE_LIST.
+        scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+        // Assert that the onConfigurationChange was called with a new correct config.
+        scenario.onActivity {
+            val lastConfig = it.lastConfigurationChangeAndClear
+            assertConfigurationLocalesEquals(
+                expectedLocales,
+                lastConfig!!
+            )
+        }
+
+        // Set locales back to system locales.
+        scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+        // Assert that the onConfigurationChange was called with a new correct config.
+        scenario.onActivity {
+            val lastConfig = it.lastConfigurationChangeAndClear
+            assertConfigurationLocalesEquals(
+                systemLocales, lastConfig!!
+            )
+        }
+    }
+
+    @Test
+    fun testOnConfigurationChangeNotCalledWhileDestroyed() {
+        scenario.moveToState(Lifecycle.State.RESUMED)
+
+        lateinit var activity: LocalesConfigChangesActivity
+        scenario.onActivity { activity = it }
+
+        scenario.moveToState(Lifecycle.State.DESTROYED)
+
+        // Clear any previous config changes.
+        activity.lastConfigurationChangeAndClear
+
+        // Set locales to CUSTOM_LOCALE_LIST.
+        setLocales(CUSTOM_LOCALE_LIST)
+        // Assert that the onConfigurationChange was not called with a new correct config.
+        assertNull(activity.lastConfigurationChangeAndClear)
+
+        // Set locales back to system locales.
+        setLocales(LocaleListCompat.getEmptyLocaleList())
+        // Assert that the onConfigurationChange was not called with a new correct config.
+        assertNull(activity.lastConfigurationChangeAndClear)
+    }
+
+    @Test
+    fun testResourcesUpdated() {
+        // Set locales to CUSTOM_LOCALE_LIST.
+        scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+
+        // Assert that the Activity resources configuration was updated.
+        assertConfigurationLocalesEquals(
+            expectedLocales,
+            scenario.withActivity { this }
+        )
+
+        // Set locales back to system locales.
+        scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+
+        // Assert that the Activity resources configuration was updated.
+        assertConfigurationLocalesEquals(
+            systemLocales,
+            scenario.withActivity { this }
+        )
+    }
+
+    @Test
+    fun testOnLocalesChangedCalled() {
+        // Set locales to CUSTOM_LOCALE_LIST.
+        scenario.onActivity { setLocales(CUSTOM_LOCALE_LIST) }
+        // Assert that the Activity received a new value.
+        assertEquals(
+            expectedLocales,
+            scenario.withActivity { lastLocalesAndReset }
+        )
+
+        // Set locales back to system locales.
+        scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+        // Assert that the Activity received a new value.
+        assertEquals(systemLocales, scenario.withActivity { lastLocalesAndReset })
+    }
+
+    @After
+    fun cleanup() {
+        // Reset the system locales.
+        if (scenario.state != Lifecycle.State.DESTROYED) {
+            scenario.onActivity { setLocales(LocaleListCompat.getEmptyLocaleList()) }
+        }
+        LocalesUpdateActivity.teardown()
+        scenario.close()
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt
new file mode 100644
index 0000000..c20886a
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesConfigChangesWithoutLayoutDirectionTestCase.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.filters.SdkSuppress
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+//  setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesConfigChangesWithoutLayoutDirectionTestCase {
+    private lateinit var scenario: ActivityScenario<
+        LocalesConfigChangesActivityWithoutLayoutDirection>
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setup() {
+        LocalesUtils.initCustomLocaleList()
+        // By default we'll set the apps to use system locales, which allows us to make better
+        // assumptions in the tests below.
+        // Launch the test activity.
+        scenario =
+            ActivityScenario.launch(LocalesConfigChangesActivityWithoutLayoutDirection::class.java)
+        scenario.onActivity {
+            // Since no locales are applied as of now, current configuration will have system
+            // locales.
+            systemLocales = LocalesUpdateActivity.getConfigLocales(it.resources.configuration)
+            // expected locales is an overlay of custom and system locales.
+            expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+                LocalesUtils.CUSTOM_LOCALE_LIST, systemLocales
+            )
+        }
+    }
+
+    @Test
+    fun testOnConfigurationChangeNotCalledWhileStarted() {
+        scenario.moveToState(Lifecycle.State.RESUMED)
+
+        // Set locales to CUSTOM_LOCALE_LIST.
+        scenario.onActivity { LocalesUtils.setLocales(LocalesUtils.CUSTOM_LOCALE_LIST) }
+        // Assert that the onConfigurationChange was called with a new correct config.
+        scenario.onActivity {
+            // the call should not have reached the LocalesUpdateActivity.onConfigurationChange()
+            // because the manifest entry for LocalesConfigChangesActivityWithoutLayoutDirection
+            // only handles locale and not layoutDir.
+            assertNull(it.lastConfigurationChangeAndClear)
+            LocalesUtils.assertConfigurationLocalesEquals(
+                expectedLocales,
+                it.resources.configuration!!
+            )
+        }
+    }
+
+    @After
+    fun teardown() {
+        LocalesUpdateActivity.teardown()
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java
new file mode 100644
index 0000000..e39c081
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationActivity.java
@@ -0,0 +1,39 @@
+/*
+ * 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.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * An activity that has a customized fontScale, set before onCreate().
+ */
+@RequiresApi(17)
+public class LocalesCustomApplyOverrideConfigurationActivity extends LocalesUpdateActivity {
+    public static final float CUSTOM_FONT_SCALE = 4.24f;
+
+    @Override
+    protected void attachBaseContext(Context newBase) {
+        super.attachBaseContext(newBase);
+
+        Configuration config = new Configuration();
+        config.fontScale = CUSTOM_FONT_SCALE;
+        super.applyOverrideConfiguration(config);
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt
new file mode 100644
index 0000000..27c3d16
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomApplyOverrideConfigurationTestCase.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.appcompat.app
+
+import android.content.res.Configuration
+import androidx.appcompat.app.LocalesCustomApplyOverrideConfigurationActivity.CUSTOM_FONT_SCALE
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.testutils.NightModeUtils
+import androidx.appcompat.testutils.NightModeUtils.assertConfigurationNightModeEquals
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * This is one approach to customize Activity's configuration that's used in google3.
+ */
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 17, maxSdkVersion = 31)
+class LocalesCustomApplyOverrideConfigurationTestCase() {
+
+    @get:Rule
+    val activityRule = LocalesActivityTestRule(
+        LocalesCustomApplyOverrideConfigurationActivity::class.java
+    )
+
+    @Test
+    @Suppress("DEPRECATION")
+    fun testNightModeIsMaintainedOnLocalesChange() {
+        NightModeUtils.setNightModeAndWaitForRecreate(
+            activityRule,
+            AppCompatDelegate.MODE_NIGHT_YES,
+            NightModeUtils.NightSetMode.LOCAL
+        )
+        assertConfigurationNightModeEquals(
+            Configuration.UI_MODE_NIGHT_YES,
+            activityRule.activity.resources.configuration
+        )
+        setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+        // Check that the custom configuration properties are maintained
+        assertConfigurationNightModeEquals(
+            Configuration.UI_MODE_NIGHT_YES,
+            activityRule.activity.resources.configuration
+        )
+        setLocalesAndWaitForRecreate(activityRule, LocaleListCompat.getEmptyLocaleList())
+    }
+
+    @Test
+    fun testFontScaleIsMaintained() {
+        // Check that the custom configuration properties are maintained
+        val config = activityRule.activity.resources.configuration
+        assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+    }
+
+    @Test
+    fun testFontScaleIsMaintainedOnLocalesChange() {
+        // Set locales to CUSTOM_LOCALE_LIST.
+        setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+        // Check that the custom configuration properties are maintained.
+        val config = activityRule.activity.resources.configuration
+        assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.java
new file mode 100644
index 0000000..9f929e5
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextActivity.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.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Build;
+
+/**
+ * An activity with customized configuration.
+ */
+public class LocalesCustomAttachBaseContextActivity extends LocalesUpdateActivity {
+    public static final float CUSTOM_FONT_SCALE = 4.24f;
+
+    @Override
+    protected void attachBaseContext(Context newBase) {
+        super.attachBaseContext(useCustomConfig(newBase));
+    }
+
+    private Context useCustomConfig(Context context) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            Configuration config = new Configuration();
+            config.fontScale = CUSTOM_FONT_SCALE;
+            return context.createConfigurationContext(config);
+        } else {
+            Resources res = context.getResources();
+            Configuration config = new Configuration(res.getConfiguration());
+            config.fontScale = CUSTOM_FONT_SCALE;
+            res.updateConfiguration(config, res.getDisplayMetrics());
+            return context;
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt
new file mode 100644
index 0000000..9215bf4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesCustomAttachBaseContextTestCase.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.appcompat.app
+
+import android.content.res.Configuration
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
+import androidx.appcompat.app.NightModeCustomAttachBaseContextActivity.CUSTOM_FONT_SCALE
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.testutils.NightModeUtils.NightSetMode
+import androidx.appcompat.testutils.NightModeUtils.assertConfigurationNightModeEquals
+import androidx.appcompat.testutils.NightModeUtils.setNightModeAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesCustomAttachBaseContextTestCase() {
+
+    @get:Rule
+    val activityRule = LocalesActivityTestRule(
+        LocalesCustomAttachBaseContextActivity::class.java
+    )
+
+    @Before
+    fun setUp() {
+        AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+    }
+
+    @Test
+    @Suppress("DEPRECATION")
+    fun testNightModeIsMaintainedOnLocalesChange() {
+        setNightModeAndWaitForRecreate(
+            activityRule,
+            MODE_NIGHT_YES,
+            NightSetMode.LOCAL
+        )
+        assertConfigurationNightModeEquals(
+            Configuration.UI_MODE_NIGHT_YES,
+            activityRule.activity.resources.configuration
+        )
+        setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+        // Check that the custom configuration properties are maintained
+        assertConfigurationNightModeEquals(
+            Configuration.UI_MODE_NIGHT_YES,
+            activityRule.activity.resources.configuration
+        )
+    }
+
+    @Test
+    fun testFontScaleIsMaintained() {
+        // Check that the custom configuration properties are maintained.
+        val config = activityRule.activity.resources.configuration
+        assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+    }
+
+    @Test
+    fun testFontScaleIsMaintainedOnLocalesChange() {
+        // Set locales to CUSTOM_LOCALE_LIST.
+        setLocalesAndWaitForRecreate(activityRule, CUSTOM_LOCALE_LIST)
+        // Check that the custom configuration properties are maintained.
+        val config = activityRule.activity.resources.configuration
+        assertEquals(CUSTOM_FONT_SCALE, config.fontScale)
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt
new file mode 100644
index 0000000..7ced0ff
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesForegroundDialogTestCase.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.appcompat.app
+
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.TestCase.assertNotSame
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesForegroundDialogTestCase {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    private var baseLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setUp() {
+        AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        baseLocales = LocalesUpdateActivity.getConfigLocales(rule.activity.resources.configuration)
+    }
+
+    @Test
+    fun testLocalesChangeWithForegroundDialog() {
+        val firstActivity = rule.activity
+        assertConfigurationLocalesEquals(baseLocales, firstActivity)
+
+        // Open a dialog on top of the activity.
+        rule.runOnUiThread {
+            val frag = TestDialogFragment.newInstance()
+            frag.show(firstActivity.supportFragmentManager, "dialog")
+        }
+
+        // Now change the locales for the foreground activity.
+        setLocalesAndWaitForRecreate(
+            firstActivity,
+            CUSTOM_LOCALE_LIST
+        )
+
+        // Ensure that it was recreated.
+        assertNotSame(rule.activity, firstActivity)
+        waitUntilState(rule.activity, Lifecycle.State.RESUMED)
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
new file mode 100644
index 0000000..6466e41
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateActivity.java
@@ -0,0 +1,85 @@
+/*
+ * 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.appcompat.app;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.LocaleList;
+
+import androidx.core.os.LocaleListCompat;
+
+import java.util.Locale;
+
+/**
+ * An activity with systemLocales replaced with some customised locales for testing.
+ */
+public class LocalesLateOnCreateActivity extends LocalesUpdateActivity {
+
+    public static LocaleListCompat DEFAULT_LOCALE_LIST;
+    public static LocaleListCompat TEST_LOCALE_LIST;
+    public static LocaleListCompat EXPECTED_LOCALE_LIST;
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        // Override locales so that AppCompat attempts to re-apply during onCreate().
+
+        if (Build.VERSION.SDK_INT >= 21) {
+            DEFAULT_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+                    Locale.US.toLanguageTag() + "," + Locale.CHINESE.toLanguageTag());
+            TEST_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+                    Locale.CANADA_FRENCH.toLanguageTag() + ","
+                            + Locale.US.toLanguageTag());
+            EXPECTED_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+                    Locale.CANADA_FRENCH.toLanguageTag() + ","
+                            + Locale.US.toLanguageTag() + "," + Locale.CHINESE.toLanguageTag());
+        } else {
+            DEFAULT_LOCALE_LIST = LocaleListCompat.create(Locale.US);
+            TEST_LOCALE_LIST = LocaleListCompat.create(Locale.CANADA_FRENCH);
+            EXPECTED_LOCALE_LIST = LocaleListCompat.create(Locale.CANADA_FRENCH);
+        }
+        disableAutomaticLocales(getApplicationContext());
+
+        super.onCreate(bundle);
+    }
+
+    private static void setLocales(LocaleListCompat locales, Context context) {
+        Configuration conf = context.getResources().getConfiguration();
+        if (Build.VERSION.SDK_INT >= 24) {
+            conf.setLocales(LocaleList.forLanguageTags(locales.toLanguageTags()));
+        } else if (Build.VERSION.SDK_INT >= 17) {
+            conf.setLocale(locales.get(0));
+        } else {
+            conf.locale = locales.get(0);
+        }
+        // updateConfiguration is required to make the configuration change stick.
+        // updateConfiguration must be called before any use of the actual Resources.
+        context.getResources().updateConfiguration(conf,
+                context.getResources().getDisplayMetrics());
+    }
+
+    /**
+     * Ensures the context does not use system locales, instead uses the DEFAULT_LOCALE_LIST
+     *
+     * <p>This must be called before a Context's Resources are used for the first time. {@code
+     * Activity.onCreate} is a great place to call {@code disableAutomaticLocales(this)}
+     */
+    public static void disableAutomaticLocales(Context context) {
+        setLocales(DEFAULT_LOCALE_LIST, context);
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt
new file mode 100644
index 0000000..13968ee
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesLateOnCreateTestCase.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.appcompat.app
+
+import androidx.appcompat.app.LocalesLateOnCreateActivity.DEFAULT_LOCALE_LIST
+import androidx.appcompat.app.LocalesLateOnCreateActivity.EXPECTED_LOCALE_LIST
+import androidx.appcompat.app.LocalesLateOnCreateActivity.TEST_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+//  setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesLateOnCreateTestCase {
+
+    @get:Rule
+    val activityRule = LocalesActivityTestRule(LocalesLateOnCreateActivity::class.java)
+
+    @Test
+    fun testActivityRecreateLoop() {
+        // Activity should be able to reach fully resumed state in default locales.
+        waitUntilState(activityRule.activity, Lifecycle.State.RESUMED)
+        assertConfigurationLocalesEquals(
+            DEFAULT_LOCALE_LIST,
+            activityRule.activity.resources.configuration
+        )
+
+        // Simulate the user set locales, which should force an activity recreate().
+        setLocalesAndWaitForRecreate(
+            activityRule,
+            TEST_LOCALE_LIST
+        )
+
+        // Activity should be able to reach fully resumed state again.
+        waitUntilState(activityRule.activity, Lifecycle.State.RESUMED)
+
+        // The requested locales should have been set during attachBaseContext().
+        assertConfigurationLocalesEquals(
+            EXPECTED_LOCALE_LIST,
+            activityRule.activity.resources.configuration
+        )
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
new file mode 100644
index 0000000..0470ce5f
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.content.Intent
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesPersistTestCase() {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setUp() {
+        // Since no locales are applied as of now, current configuration will have system
+        // locales.
+        systemLocales = LocalesUpdateActivity.getConfigLocales(
+            rule.activity.resources.configuration
+        )
+        expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales
+        )
+    }
+
+    /**
+     * This test verifies that the locales persist in storage when a metadata entry for
+     * "autoStoreLocale" is provided as an opt-in.
+     * To replicate the scenario of app-startup a method resetStaticRequestedAndStoredLocales()
+     * is called to clear out the static storage of locales. The flow of the test is:
+     * setApplicationLocales is called on the firstActivity and it is recreated as recreatedFirst.
+     * Now the locales must have been stored and the static storage would also hold these. Then
+     * we clear out the static storage and create a new activity secondActivity by using an intent
+     * and because the static storage was already clear the activity should sync locales from
+     * storage and start up in the app-specific locales.
+     */
+    @Test
+    fun testLocalesAppliedInNewActivityAfterStaticStorageCleared() {
+        // mimics opting in to "autoStoreLocales"
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val firstActivity = rule.activity
+        assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+        // Now change the locales for the activity
+        val recreatedFirst = setLocalesAndWaitForRecreate(
+            firstActivity,
+            CUSTOM_LOCALE_LIST
+        )
+
+        assertConfigurationLocalesEquals(expectedLocales, recreatedFirst)
+
+        // clear out static storage so that, when a new activity starts it syncs locales
+        // from storage.
+        AppCompatDelegate.resetStaticRequestedAndStoredLocales()
+        // verify that the static locales were cleared out.
+        assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+        // Start a new Activity, so that the original Activity goes into the background
+        val intent = Intent(recreatedFirst, AppCompatActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+        val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+        // assert that the new activity started with the app-specific locales after reading them
+        // from storage.
+        assertConfigurationLocalesEquals(expectedLocales, secondActivity)
+    }
+
+    @Test
+    fun testNewActivityCreatedWhenNoAppLocalesExist() {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val firstActivity = rule.activity
+        assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+        // Start a new Activity, so that the original Activity goes into the background
+        val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+        val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+        // assert that the new activity started with the systemLocales.
+        assertConfigurationLocalesEquals(systemLocales, secondActivity)
+    }
+
+    @After
+    fun teardown() {
+        rule.runOnUiThread {
+            // clean-up
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+            // setting auto storage opt-in to false
+            AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java
new file mode 100644
index 0000000..14efcde
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivity.java
@@ -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.
+ */
+
+package androidx.appcompat.app;
+
+public class LocalesRotateDoesNotRecreateActivity extends LocalesUpdateActivity {}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt
new file mode 100644
index 0000000..ef28708
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateDoesNotRecreateActivityTestCase.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.appcompat.app
+
+import androidx.appcompat.Orientation
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.withOrientation
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.LifecycleOwnerUtils
+import org.junit.After
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 18, maxSdkVersion = 31)
+class LocalesRotateDoesNotRecreateActivityTestCase() {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+    @get:Rule
+    val activityRule: LocalesActivityTestRule<LocalesRotateDoesNotRecreateActivity> =
+        LocalesActivityTestRule(
+            LocalesRotateDoesNotRecreateActivity::class.java,
+            initialTouchMode = false,
+            // Let the test method launch its own activity so that we can ensure it's RESUMED.
+            launchActivity = false
+        )
+
+    @Test
+    fun testRotateDoesNotRecreateActivity() {
+        // Set locales to CUSTOM_LOCALE_LIST and wait for state RESUMED.
+        val initialActivity = activityRule.launchActivity(null)
+        var systemLocales = LocalesUpdateActivity.getConfigLocales(
+            initialActivity.resources.configuration)
+        LifecycleOwnerUtils.waitUntilState(initialActivity, Lifecycle.State.RESUMED)
+        setLocalesAndWaitForRecreate(initialActivity, CUSTOM_LOCALE_LIST)
+
+        val localesActivity = activityRule.activity
+        val config = localesActivity.resources.configuration
+
+        // On API level 26 and below, the configuration object is going to be identical
+        // across configuration changes, so we need to pull the orientation value now.
+        val orientation = config.orientation
+        val expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales)
+        // Assert that the current Activity has the new locales.
+        assertConfigurationLocalesEquals(expectedLocales, config)
+
+        // Now rotate the device. This should NOT result in a lifecycle event, just a call to
+        // onConfigurationChanged.
+        localesActivity.resetOnConfigurationChange()
+        device.withOrientation(Orientation.LEFT) {
+            instrumentation.waitForIdleSync()
+            localesActivity.expectOnConfigurationChange(5000)
+
+            // Assert that we got the same activity and thus it was not recreated.
+            val rotatedLocalesActivity = activityRule.activity
+            val rotatedConfig = rotatedLocalesActivity.resources.configuration
+            assertSame(localesActivity, rotatedLocalesActivity)
+            assertConfigurationLocalesEquals(expectedLocales, rotatedConfig)
+
+            // On API level 26 and below, the configuration object is going to be identical
+            // across configuration changes, so we need to compare against the cached value.
+            assertNotSame(orientation, rotatedConfig.orientation)
+        }
+    }
+
+    @After
+    fun teardown() {
+        device.setOrientationNatural()
+        // setOrientationNatural may need some time rotate orientation to natural, so we wait for
+        // the operation to end for 5000ms.
+        device.waitForIdle(/* timeout= */5000)
+
+        // Clean up
+        activityRule.runOnUiThread {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt
new file mode 100644
index 0000000..d5f5fa1
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesRotateRecreatesActivityWithConfigTestCase.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.appcompat.app
+
+import android.app.Activity
+import android.app.Instrumentation
+import androidx.appcompat.Orientation
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.appcompat.withOrientation
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.LifecycleOwnerUtils
+import org.junit.After
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(minSdkVersion = 18, maxSdkVersion = 31)
+class LocalesRotateRecreatesActivityWithConfigTestCase() {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    @get:Rule
+    public val activityRule: LocalesActivityTestRule<LocalesUpdateActivity> =
+        LocalesActivityTestRule(
+            LocalesUpdateActivity::class.java,
+            initialTouchMode = false,
+            // Let the test method launch its own activity so that we can ensure it's RESUMED.
+            launchActivity = false
+        )
+
+    @After
+    public fun teardown() {
+        device.setOrientationNatural()
+        device.waitForIdle(/* timeout= */5000)
+
+        // Clean up after the default mode test.
+
+        activityRule.runOnUiThread {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+    }
+
+    @Test
+    public fun testRotateRecreatesActivityWithConfig() {
+        // Set locales to CUSTOM_LOCALE_LIST and wait for state RESUMED.
+        val initialActivity = activityRule.launchActivity(null)
+        LifecycleOwnerUtils.waitUntilState(initialActivity, Lifecycle.State.RESUMED)
+        systemLocales = LocalesUpdateActivity.getConfigLocales(activityRule.activity.resources
+            .configuration)
+
+        setLocalesAndWaitForRecreate(initialActivity, CUSTOM_LOCALE_LIST)
+
+        val localesActivity = activityRule.activity
+        val config = localesActivity.resources.configuration
+
+        // On API level 26 and below, the configuration object is going to be identical
+        // across configuration changes, so we need to pull the orientation value now.
+        val orientation = config.orientation
+
+        // Assert that the current Activity has the expected locales.
+        assertConfigurationLocalesEquals(LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales), config)
+
+        // Now rotate the device. This should result in an onDestroy lifecycle event.
+        localesActivity.resetOnDestroy()
+        rotateDeviceAndWaitForRecreate(localesActivity) {
+            localesActivity.expectOnDestroy(/* timeout= */ 5000)
+
+            // Assert that we got a different activity and thus it was recreated.
+            val rotatedLocalesActivity = activityRule.activity
+            val rotatedConfig = rotatedLocalesActivity.resources.configuration
+            assertNotSame(localesActivity, rotatedLocalesActivity)
+            assertConfigurationLocalesEquals(
+                LocalesUpdateActivity.overlayCustomAndSystemLocales(CUSTOM_LOCALE_LIST,
+                    systemLocales),
+                rotatedConfig
+            )
+
+            // On API level 26 and below, the configuration object is going to be identical
+            // across configuration changes, so we need to compare against the cached value.
+            assertNotSame(orientation, rotatedConfig.orientation)
+        }
+    }
+
+    private fun rotateDeviceAndWaitForRecreate(activity: Activity, doThis: () -> Unit) {
+        val monitor = Instrumentation.ActivityMonitor(activity::class.java.name, /* result= */
+            null, /* block= */false)
+        instrumentation.addMonitor(monitor)
+
+        device.withOrientation(Orientation.LEFT) {
+            // Wait for the activity to be recreated after rotation
+            var count = 0
+            var lastActivity: Activity? = activity
+            while ((lastActivity == null || activity == lastActivity) && count < 5) {
+                // If this times out, it will return null.
+                lastActivity = monitor.waitForActivityWithTimeout(/* timeout= */ 1000L)
+                count++
+            }
+            instrumentation.waitForIdleSync()
+
+            // Ensure that we didn't time out
+            assertNotNull("Activity was not recreated within 5000ms", lastActivity)
+            assertNotEquals(
+                "Activity was not recreated within 5000ms", activity, lastActivity
+            )
+            doThis()
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt
new file mode 100644
index 0000000..82fd8e4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSetUsingFrameworkApiTestCase.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.os.LocaleList
+import androidx.annotation.RequiresApi
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.BuildCompat
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales. The minSdkVersion should be set to 33 after API bump.
+@SdkSuppress(minSdkVersion = 32)
+class LocalesSetUsingFrameworkApiTestCase {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @RequiresApi(33)
+    @Before
+    fun setUp() {
+        // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+        //  bumped to 33
+        assumeTrue(
+            "Requires API version >=T", BuildCompat.isAtLeastT()
+        )
+
+        // setting the app to follow system.
+        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+            AppCompatDelegate.getLocaleManagerForApplication(),
+            LocaleList.getEmptyLocaleList()
+        )
+        // Since no locales are applied as of now, current configuration will have system
+        // locales.
+        systemLocales = LocalesUpdateActivity.getConfigLocales(
+            rule.activity.resources.configuration
+        )
+        expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            LocalesUtils.CUSTOM_LOCALE_LIST, systemLocales
+        )
+    }
+
+    /**
+     * Verifies that for API version >=T the AppCompatDelegate.setApplicationLocales() call
+     * is redirected to the framework API and the locales are applied successfully.
+     */
+    @Test
+    @RequiresApi(33)
+    fun testSetApplicationLocales_postT_frameworkApiCalled() {
+        val firstActivity = rule.activity
+        assertConfigurationLocalesEquals(systemLocales, firstActivity)
+
+        assertEquals(
+            LocaleListCompat.getEmptyLocaleList(),
+            AppCompatDelegate.getApplicationLocales()
+        )
+        assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+        // Now change the locales for the activity
+        val recreatedFirst = LocalesUtils.setLocalesAndWaitForRecreate(
+            firstActivity,
+            CUSTOM_LOCALE_LIST
+        )
+
+        assertEquals(
+            CUSTOM_LOCALE_LIST,
+            AppCompatDelegate.getApplicationLocales()
+        )
+        // check that the locales were set using the framework API
+        assertEquals(
+            CUSTOM_LOCALE_LIST.toLanguageTags(),
+            AppCompatDelegate.Api33Impl.localeManagerGetApplicationLocales(
+                AppCompatDelegate.getLocaleManagerForApplication()
+            ).toLanguageTags()
+        )
+        // check locales are applied successfully
+        assertConfigurationLocalesEquals(expectedLocales, recreatedFirst)
+        // check that the override was not done by AndroidX, but by the framework
+        assertNull(AppCompatDelegate.getRequestedAppLocales())
+    }
+
+    @RequiresApi(33)
+    @After
+    fun teardown() {
+        // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+        //  bumped to 33
+        if (!BuildCompat.isAtLeastT()) {
+            return
+        }
+        // clearing locales from framework. setting the app to follow system.
+        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+            AppCompatDelegate.getLocaleManagerForApplication(),
+            LocaleList.getEmptyLocaleList()
+        )
+    }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt
new file mode 100644
index 0000000..a8cb155
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesStackedHandlingTestCase.kt
@@ -0,0 +1,367 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.app.Instrumentation.ActivityMonitor
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils.waitUntilState
+import junit.framework.Assert.assertNotNull
+import junit.framework.Assert.assertNotSame
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesStackedHandlingTestCase {
+
+    @Before
+    fun setUp() {
+        LocalesUtils.initCustomLocaleList()
+    }
+
+    /**
+     * Regression test for the following scenario:
+     *
+     * If you have a stack of activities which includes one with android:configChanges="locale" and
+     * android:configChanges="layoutDirection" and you call AppCompatDelegate.setApplicationLocales
+     * it can cause other activities to not be recreated.
+     *
+     * Eg:
+     * - Activity A DOESN'T intercept locales changes and layoutDirection changes in manifest
+     * - Activity B DOESN'T intercept locales changes and layoutDirection changes in manifest
+     * - Activity C DOES intercept both locales and layoutDirection changes in manifest
+     *
+     * Here is your stack : A > B > C (C on top)
+     *
+     * Call AppCompatDelegate.setApplicationLocales with a new mode on activity C. Activity C
+     * receives the change in onConfigurationChanged but there is a good chance that activity A
+     * and/or B were not recreated.
+     */
+    @Test
+    fun testLocalesWithStackedActivities() {
+        val instr = InstrumentationRegistry.getInstrumentation()
+        val result = Instrumentation.ActivityResult(0, Intent())
+        val monitorA = ActivityMonitor(LocalesActivityA::class.java.name, result, false)
+        val monitorB = ActivityMonitor(LocalesActivityB::class.java.name, result, false)
+        val monitorC = ActivityMonitor(
+            LocalesConfigChangesActivity::class.java.name,
+            result, false
+        )
+        instr.addMonitor(monitorA)
+        instr.addMonitor(monitorB)
+        instr.addMonitor(monitorC)
+
+        instr.runOnMainSync {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+
+        // Start activity A.
+        instr.startActivitySync(
+            Intent(instr.context, LocalesActivityA::class.java).apply {
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "A")
+            }
+        )
+
+        // From activity A, start activity B.
+        val activityA = monitorA.waitForActivityWithTimeout(/* timeout= */ 3000)
+            as LocalesUpdateActivity
+        assertNotNull(activityA)
+        activityA.startActivity(
+            Intent(instr.context, LocalesActivityB::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "B")
+            }
+        )
+
+        var systemLocales = LocalesUpdateActivity.getConfigLocales(
+            activityA.resources.configuration
+        )
+
+        // Activity A is hidden, wait for it to stop.
+        waitUntilState(activityA, Lifecycle.State.CREATED)
+
+        // From activity B, start activity C.
+        val activityB =
+            monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotNull(activityB)
+        activityB.startActivity(
+            Intent(instr.context, LocalesConfigChangesActivity::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "C")
+            }
+        )
+
+        // Activity B is hidden, wait for it to stop.
+        waitUntilState(activityB, Lifecycle.State.CREATED)
+
+        // apply CUSTOM_LOCALE_LIST
+        val activityC =
+            monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotNull(activityC)
+        activityC.runOnUiThread {
+            AppCompatDelegate.setApplicationLocales(CUSTOM_LOCALE_LIST)
+        }
+
+        // Activity C should receive a configuration change.
+        activityC.expectOnConfigurationChange(/* timeout= */ 3000)
+
+        // Activities A and B should recreate() in the background.
+        val activityA2 = expectRecreate(monitorA, activityA) as LocalesUpdateActivity
+        val activityB2 = expectRecreate(monitorB, activityB) as LocalesUpdateActivity
+
+        var expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales
+        )
+        // Activity C should have received a locales configuration change.
+        listOf(activityC, activityA2, activityB2).forEach { activity ->
+            activityC.runOnUiThread {
+                assertConfigurationLocalesEquals(
+                    "Activity ${activity.title}'s effective configuration has locales set",
+                    expectedLocales,
+                    activityC.effectiveConfiguration!!
+                )
+            }
+        }
+    }
+
+    /**
+     * Regression test for the following scenario:
+     *
+     * If you have a stack of activities where every activity has `android:configChanges="locale"`
+     * and android:configChanges="layoutDirection" and you call
+     * [AppCompatDelegate.setApplicationLocales] from thread other than the top activity,
+     * then it can cause the bottom activity to not receive `onConfigurationChanged`.
+     *
+     * Eg:
+     * - Activity A DOES intercept locales and layoutDirection changes in manifest
+     * - Activity B DOES intercept locales and layoutDirection changes in manifest
+     * - Activity C DOES intercept locales and layoutDirection changes in manifest
+     *
+     * Here is your stack : A > B > C (C on top)
+     *
+     * Call [AppCompatDelegate.setApplicationLocales] with a new mode on activity C (but not
+     * directly
+     * from this activity, ex with RX AndroidSchedulers.mainThread or an handler). Activity C
+     * receives both `onConfigurationChanged` and `onLocalesChanged`, but activities A and B
+     * may not receive either callback or change their configurations.
+     *
+     * Process:
+     * 1. A > B > C > setApplicationLocales YES
+     * 2. Go back to A (B & C destroyed) > B > C > setApplicationLocales NO (wrong config for A)
+     * 3. repeat (YES/NO/YES/NO...)
+     */
+    @Test
+    fun testLocalesWithStackedActivitiesAndNavigation() {
+        val instr = InstrumentationRegistry.getInstrumentation()
+        val result = Instrumentation.ActivityResult(0, Intent())
+        val monitorA = ActivityMonitor(
+            LocalesConfigChangesActivity::class.java.name,
+            result, false
+        )
+        val monitorB = ActivityMonitor(
+            LocalesConfigChangesActivityA::class.java.name,
+            result, false
+        )
+        val monitorC = ActivityMonitor(
+            LocalesConfigChangesActivityB::class.java.name,
+            result, false
+        )
+        instr.addMonitor(monitorA)
+        instr.addMonitor(monitorB)
+        instr.addMonitor(monitorC)
+
+        instr.runOnMainSync {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+
+        // Start activity A.
+        instr.startActivitySync(
+            Intent(instr.context, LocalesConfigChangesActivity::class.java).apply {
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "A")
+            }
+        )
+
+        // From activity A, start activity B.
+        val activityA =
+            monitorA.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotNull("Activity A started within 3000ms", activityA)
+        activityA.startActivity(
+            Intent(instr.context, LocalesConfigChangesActivityA::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "B")
+            }
+        )
+
+        var systemLocales = LocalesUpdateActivity.getConfigLocales(
+            activityA.resources.configuration
+        )
+
+        // Activity A is hidden, wait for it to stop.
+        waitUntilState(activityA, Lifecycle.State.CREATED)
+
+        // From activity B, start activity C.
+        val activityB =
+            monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotNull("Activity B started within 3000ms", activityB)
+        activityB.startActivity(
+            Intent(instr.context, LocalesConfigChangesActivityB::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "C")
+            }
+        )
+
+        // Activity B is hidden, wait for it to stop.
+        waitUntilState(activityB, Lifecycle.State.CREATED)
+
+        // Wait for activity C to start.
+        val activityC =
+            monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotNull("Activity C started within 3000ms", activityC)
+
+        // Change locales from a non-UI thread.
+        Handler(Looper.getMainLooper()).post {
+            AppCompatDelegate.setApplicationLocales(CUSTOM_LOCALE_LIST)
+        }
+
+        // Activities A, B, and C should all receive configuration changes.
+        listOf(activityA, activityB, activityC).forEach { activity ->
+            activity.expectOnConfigurationChange(/* timeout= */ 3000)
+        }
+
+        var expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales
+        )
+
+        // Activities A, B, and C should have all received the new configuration.
+        listOf(activityA, activityB, activityC).forEach { activity ->
+            activity.runOnUiThread {
+                assertConfigurationLocalesEquals(
+                    "Activity ${activity.title}'s effective configuration has locales set",
+                    expectedLocales,
+                    activity.effectiveConfiguration!!
+                )
+            }
+        }
+
+        // Tear down activities C and B, in that order.
+        listOf(activityC, activityB).forEach { activity ->
+            activity.runOnUiThread {
+                activity.finish()
+            }
+            waitUntilState(activity, Lifecycle.State.DESTROYED)
+        }
+
+        // Activity A is in the foreground, wait for it to resume.
+        waitUntilState(activityA, Lifecycle.State.RESUMED)
+
+        // From activity A, start activity B again.
+        activityA.startActivity(
+            Intent(instr.context, LocalesConfigChangesActivityA::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "B2")
+            }
+        )
+
+        // Activity A is hidden, wait for it to stop.
+        waitUntilState(activityA, Lifecycle.State.CREATED)
+
+        // From activity B, start activity C. Double-check the return, since the monitor could
+        // trigger on Activity B's lifecycle if the platform does something unexpected.
+        val activityB2 =
+            monitorB.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotSame("Monitor responded to activity B2 lifecycle", activityB, activityB2)
+        assertNotNull("Activity B2 started within 3000ms", activityB2)
+        activityB2.startActivity(
+            Intent(instr.context, LocalesConfigChangesActivityB::class.java).apply {
+                putExtra(LocalesUpdateActivity.KEY_TITLE, "C2")
+            }
+        )
+
+        // Activity B is hidden, wait for it to stop.
+        waitUntilState(activityB2, Lifecycle.State.CREATED)
+
+        // Wait for activity C to start. Double-check the return.
+        val activityC2 =
+            monitorC.waitForActivityWithTimeout(/* timeout= */ 3000) as LocalesUpdateActivity
+        assertNotSame("Monitor responded to Activity C2 lifecycle", activityC, activityC2)
+        assertNotNull("Activity C2 started within 3000ms", activityC2)
+
+        // Prepare activities A, B, and C to track configuration changes.
+        listOf(activityA, activityB2, activityC2).forEach { activity ->
+            activity.resetOnConfigurationChange()
+        }
+
+        // Change locales again from a non-UI thread.
+        Handler(Looper.getMainLooper()).post {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+
+        // Activities A, B, and C should all receive configuration changes.
+        listOf(activityA, activityB2, activityC2).forEach { activity ->
+            activity.expectOnConfigurationChange(/* timeout= */ 3000)
+        }
+
+        // Activities A, B, and C should have all received the new configuration.
+        listOf(activityA, activityB2, activityC2).forEach { activity ->
+            activity.runOnUiThread {
+                assertConfigurationLocalesEquals(
+                    "Activity ${activity.title}'s effective configuration has locales set",
+                    systemLocales,
+                    activity.effectiveConfiguration!!
+                )
+            }
+        }
+    }
+
+    private fun expectRecreate(monitor: ActivityMonitor, activity: Activity): Activity {
+        // The documentation says "Block until an Activity is created that matches this monitor."
+        // This statement is true, but there are some other true statements like: "Block until an
+        // Activity is destroyed" or "Block until an Activity is resumed"...
+        var activityResult: Activity?
+        synchronized(monitor) {
+            do {
+                // this call will release synchronization monitor's monitor
+                activityResult = monitor.waitForActivityWithTimeout(/* timeout= */ 3000)
+            } while (activityResult != null && activityResult == activity)
+        }
+
+        assertNotNull("Recreated activity " + activity.title, activityResult)
+        return activityResult!!
+    }
+
+    @After
+    fun teardown() {
+        LocalesUpdateActivity.teardown()
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
new file mode 100644
index 0000000..7c51c8b
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("deprecation")
+
+package androidx.appcompat.app
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.LocaleList
+import androidx.annotation.RequiresApi
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.os.BuildCompat
+import androidx.core.os.LocaleListCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNull
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test case to verify app-locales sync to framework on Version upgrade from Pre T to T.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+// setApplicationLocales
+// This test should only be run for API version T and hence after API bump both
+// minSdkVersion and maxSdkVersion should be set to 33.
+@SdkSuppress(minSdkVersion = 32, maxSdkVersion = 33)
+class LocalesSyncToFrameworkTestCase {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    private var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    private var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+    private lateinit var appLocalesComponent: ComponentName
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @RequiresApi(33)
+    @Before
+    fun setUp() {
+        // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+        //  bumped to 33
+        assumeTrue("Requires API version >=T", BuildCompat.isAtLeastT())
+        // setting the app to follow system.
+        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+            AppCompatDelegate.getLocaleManagerForApplication(),
+            LocaleList.getEmptyLocaleList()
+        )
+
+        // Since no locales are applied as of now, current configuration will have system
+        // locales.
+        systemLocales = LocalesUpdateActivity.getConfigLocales(
+            rule.activity.resources.configuration
+        )
+        expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(
+            CUSTOM_LOCALE_LIST, systemLocales
+        )
+
+        appLocalesComponent = ComponentName(
+            instrumentation.context,
+            AppLocalesStorageHelper.APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME
+        )
+    }
+
+    @RequiresApi(33)
+    @Test
+    fun testAutoSync_preTToPostT_syncsSuccessfully() {
+        val firstActivity = rule.activity
+
+        // activity is following the system and the requested locales are null.
+        assertConfigurationLocalesEquals(systemLocales, firstActivity)
+        assertNull(AppCompatDelegate.getRequestedAppLocales())
+
+        val context = instrumentation.context
+
+        // persist some app locales in storage, mimicking locales set using the backward
+        // compatibility API
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+        AppLocalesStorageHelper.persistLocales(context, CUSTOM_LOCALE_LIST.toLanguageTags())
+
+        // explicitly disable appLocalesComponent that acts as a marker to represent that the
+        // locales has been synced so that when a new activity is created the locales are
+        // synced from storage
+        context.packageManager.setComponentEnabledSetting(
+            appLocalesComponent,
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+            /* flags= */ PackageManager.DONT_KILL_APP
+        )
+
+        // resetting static storage represents a fresh app start up.
+        AppCompatDelegate.resetStaticRequestedAndStoredLocales()
+
+        // Start a new Activity, so that the original Activity goes into the background
+        val intent = Intent(firstActivity, AppCompatActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+        val secondActivity = instrumentation.startActivitySync(intent) as AppCompatActivity
+
+        // wait for locales to get synced, stop execution of the current thread for the
+        // timeout period
+        Thread.sleep(/* timeout= */ 1000)
+
+        // check that the locales were set using the framework API and they have been synced
+        // successfully
+        assertEquals(
+            CUSTOM_LOCALE_LIST.toLanguageTags(),
+            AppCompatDelegate.Api33Impl.localeManagerGetApplicationLocales(
+                AppCompatDelegate.getLocaleManagerForApplication()
+            ).toLanguageTags()
+        )
+        // check that the activity has the app specific locales
+        assertConfigurationLocalesEquals(expectedLocales, secondActivity)
+        // check that the override was not done by AndroidX, but by the framework
+        assertNull(AppCompatDelegate.getRequestedAppLocales())
+        // check that the synced marker was set to true
+        assertEquals(
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+            context.packageManager.getComponentEnabledSetting(appLocalesComponent)
+        )
+
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+    }
+
+    @After
+    @RequiresApi(33)
+    fun teardown() {
+        // TODO(b/223775393): Remove BuildCompat.isAtLeastT() checks after API version is
+        //  bumped to 33
+        if (!BuildCompat.isAtLeastT()) {
+            return
+        }
+        val context = instrumentation.context
+
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+        // setting empty locales deletes the persisted locales record.
+        AppLocalesStorageHelper.persistLocales(context, /* empty locales */ "")
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(false)
+
+        // clearing locales from framework.
+        // setting the app to follow system.
+        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+            AppCompatDelegate.getLocaleManagerForApplication(),
+            LocaleList.getEmptyLocaleList()
+        )
+
+        // disabling component enabled setting for app_locales sync marker.
+        context.packageManager.setComponentEnabledSetting(
+            appLocalesComponent,
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+            /* flags= */ PackageManager.DONT_KILL_APP
+        )
+    }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java
new file mode 100644
index 0000000..7678876
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateActivity.java
@@ -0,0 +1,217 @@
+/*
+ * 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.appcompat.app;
+
+import static androidx.appcompat.app.LocaleOverlayHelper.combineLocalesIfOverlayExists;
+
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.test.R;
+import androidx.appcompat.testutils.BaseTestActivity;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class LocalesUpdateActivity extends BaseTestActivity {
+
+
+    public static final String KEY_TITLE = "title";
+
+    private final Semaphore mOnConfigurationChangeSemaphore = new Semaphore(0);
+    private final Semaphore mOnDestroySemaphore = new Semaphore(0);
+    private final Semaphore mOnCreateSemaphore = new Semaphore(0);
+
+    private LocaleListCompat mLastLocales = LocaleListCompat.getEmptyLocaleList();
+
+    private Configuration mEffectiveConfiguration;
+    private Configuration mLastConfigurationChange;
+
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.activity_locales;
+    }
+
+    @Override
+    public void onLocalesChanged(@NonNull LocaleListCompat locales) {
+        mLastLocales = locales;
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mLastConfigurationChange = new Configuration(newConfig);
+        mEffectiveConfiguration = mLastConfigurationChange;
+        mOnConfigurationChangeSemaphore.release();
+    }
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        String title = getIntent().getStringExtra(KEY_TITLE);
+        if (title != null) {
+            setTitle(title);
+        }
+
+        mEffectiveConfiguration = new Configuration(getResources().getConfiguration());
+        mOnCreateSemaphore.release();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        mOnDestroySemaphore.release();
+    }
+
+    @Nullable
+    Configuration getLastConfigurationChangeAndClear() {
+        final Configuration config = mLastConfigurationChange;
+        mLastConfigurationChange = null;
+        return config;
+    }
+
+    /**
+     * @return a copy of the {@link Configuration} from the most recent call to {@link #onCreate} or
+     * {@link #onConfigurationChanged}, or {@code null} if neither has been called yet
+     */
+    @Nullable
+    Configuration getEffectiveConfiguration() {
+        return mEffectiveConfiguration;
+    }
+
+    LocaleListCompat getLastLocalesAndReset() {
+        final LocaleListCompat locales = mLastLocales;
+        mLastLocales = LocaleListCompat.getEmptyLocaleList();
+        return locales;
+    }
+
+    public static LocaleListCompat getConfigLocales(Configuration conf) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return AppCompatDelegateImpl.Api24Impl.getLocales(conf);
+        } else if (Build.VERSION.SDK_INT >= 21) {
+            return LocaleListCompat.forLanguageTags(AppCompatDelegateImpl.Api21Impl
+                    .toLanguageTag(conf.locale));
+        } else {
+            return LocaleListCompat.create(conf.locale);
+        }
+    }
+
+    public static LocaleListCompat overlayCustomAndSystemLocales(LocaleListCompat customLocales,
+            LocaleListCompat baseLocales) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return combineLocalesIfOverlayExists(customLocales, baseLocales);
+        } else {
+            return LocaleListCompat.create(customLocales.get(0));
+        }
+    }
+
+    /**
+     * Resets the number of received configuration changes.
+     * <p>
+     * Call this method before {@link #expectOnConfigurationChange(long)} to ensure only future
+     * configuration changes are counted.
+     */
+    public void resetOnConfigurationChange() {
+        mOnConfigurationChangeSemaphore.drainPermits();
+    }
+
+    /**
+     * Blocks until a single configuration change has been received.
+     * <p>
+     * Configuration changes are sticky; if any configuration changes were received prior to
+     * calling this method and {@link #resetOnConfigurationChange()} has not been called, this
+     * method will return immediately.
+     *
+     * @param timeout maximum amount of time (in ms) to wait for a configuration change
+     * @throws InterruptedException if the lock is interrupted
+     */
+    public void expectOnConfigurationChange(long timeout) throws InterruptedException {
+        if (Thread.currentThread() == getMainLooper().getThread()) {
+            throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+        }
+
+        mOnConfigurationChangeSemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Resets the number of received onCreate lifecycle events.
+     * <p>
+     * Call this method before {@link #expectOnCreate(long)} to ensure only future
+     * onCreate lifecycle events are counted.
+     */
+    public void resetOnCreate() {
+        mOnCreateSemaphore.drainPermits();
+    }
+
+    /**
+     * Blocks until a single onCreate lifecycle event has been received.
+     * <p>
+     * Lifecycle events are sticky; if any events were received prior to calling this method and
+     * an event has been received, this method will return immediately.
+     *
+     * @param timeout maximum amount of time (in ms) to wait for an onCreate event
+     * @throws InterruptedException if the lock is interrupted
+     */
+    public void expectOnCreate(long timeout) throws InterruptedException {
+        if (Thread.currentThread() == getMainLooper().getThread()) {
+            throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+        }
+
+        mOnCreateSemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Resets the number of received onDestroy lifecycle events.
+     * <p>
+     * Call this method before {@link #expectOnDestroy(long)} to ensure only future
+     * onDestroy lifecycle events are counted.
+     */
+    public void resetOnDestroy() {
+        mOnDestroySemaphore.drainPermits();
+    }
+
+    /**
+     * Blocks until a single onDestroy lifecycle event has been received.
+     * <p>
+     * Lifecycle events are sticky; if any events were received prior to calling this method and
+     * an event has been received, this method will return immediately.
+     *
+     * @param timeout maximum amount of time (in ms) to wait for an onDestroy event
+     * @throws InterruptedException if the lock is interrupted
+     */
+    public void expectOnDestroy(long timeout) throws InterruptedException {
+        if (Thread.currentThread() == getMainLooper().getThread()) {
+            throw new IllegalStateException("Method cannot be called on the Activity's UI thread");
+        }
+
+        mOnDestroySemaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Teardown method to clean up persistent locales in static storage.
+     */
+    public static void teardown() {
+        AppCompatDelegate.resetStaticRequestedAndStoredLocales();
+    }
+
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
new file mode 100644
index 0000000..d504dc6
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+package androidx.appcompat.app
+
+import android.webkit.WebView
+import androidx.appcompat.testutils.LocalesActivityTestRule
+import androidx.appcompat.testutils.LocalesUtils
+import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
+import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWait
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.os.LocaleListCompat
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.waitForExecution
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+// TODO(b/218430372): Modify SdkSuppress annotation in tests for backward compatibility of
+//  setApplicationLocales
+@SdkSuppress(maxSdkVersion = 31)
+class LocalesUpdateTestCase() {
+    @get:Rule
+    val rule = LocalesActivityTestRule(LocalesUpdateActivity::class.java)
+    var systemLocales = LocaleListCompat.getEmptyLocaleList()
+    var expectedLocales = LocaleListCompat.getEmptyLocaleList()
+
+    @Before
+    fun setUp() {
+        // Since no locales are applied as of now, current configuration will have system
+        // locales.
+        systemLocales = LocalesUpdateActivity.getConfigLocales(rule.activity
+            .resources.configuration)
+        expectedLocales = LocalesUpdateActivity.overlayCustomAndSystemLocales(CUSTOM_LOCALE_LIST,
+            systemLocales)
+    }
+    @Test
+    fun testDialogDoesNotOverrideActivityConfiguration() {
+        setLocalesAndWaitForRecreate(rule, CUSTOM_LOCALE_LIST)
+        // Now show a AppCompatDialog
+        lateinit var dialog: AppCompatDialog
+        rule.runOnUiThread {
+            dialog = AppCompatDialog(rule.activity)
+            dialog.show()
+        }
+        rule.waitForExecution()
+        // Now dismiss the dialog
+        rule.runOnUiThread { dialog.dismiss() }
+
+        // Assert that the locales are unchanged
+        assertConfigurationLocalesEquals(
+            expectedLocales,
+            rule.activity.resources.configuration
+        )
+    }
+
+    @Test
+    fun testLoadingWebViewMaintainsConfiguration() {
+        setLocalesAndWaitForRecreate(rule, CUSTOM_LOCALE_LIST)
+
+        // Now load a WebView into the Activity
+        rule.runOnUiThread { WebView(rule.activity) }
+
+        // Now assert that the context still has applied locales.
+        assertEquals(
+            expectedLocales,
+            LocalesUpdateActivity.getConfigLocales(rule.activity.resources.configuration)
+        )
+    }
+
+    @Test
+    fun testOnLocalesChangedCalled() {
+        val activity = rule.activity
+        // Set local night mode to YES
+        setLocalesAndWait(rule, CUSTOM_LOCALE_LIST)
+        // Assert that the Activity received a new value
+        assertEquals(expectedLocales, activity.lastLocalesAndReset)
+    }
+
+    @Test
+    fun testOnConfigurationChangeNotCalled() {
+        var activity = rule.activity
+        // Set locales to CUSTOM_LOCALE_LIST.
+        LocalesUtils.setLocalesAndWait(
+            rule,
+            CUSTOM_LOCALE_LIST
+        )
+        // Assert that onConfigurationChange was not called on the original activity
+        assertNull(activity.lastConfigurationChangeAndClear)
+
+        activity = rule.activity
+        // Set locales back to system locales.
+        setLocalesAndWait(
+            rule,
+            LocaleListCompat.getEmptyLocaleList()
+        )
+        // Assert that onConfigurationChange was not called
+        assertNull(activity.lastConfigurationChangeAndClear)
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
index 2ccfc39..a0e190b 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeForegroundDialogTestCase.kt
@@ -44,7 +44,7 @@
 
         // Open a dialog on top of the activity.
         rule.runOnUiThread {
-            val frag = NightModeDialogFragment.newInstance()
+            val frag = TestDialogFragment.newInstance()
             frag.show(firstActivity.supportFragmentManager, "dialog")
         }
 
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.java
similarity index 70%
rename from appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java
rename to appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.java
index ba3932e..907c068 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeDialogFragment.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/TestDialogFragment.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.
@@ -23,22 +23,26 @@
 import androidx.annotation.Nullable;
 import androidx.fragment.app.DialogFragment;
 
-public class NightModeDialogFragment extends DialogFragment {
+/**
+ * Test class extending DialogFragment used for testing of configuration changes like nightMode and
+ * locales.
+ */
+public class TestDialogFragment extends DialogFragment {
 
-    public NightModeDialogFragment() {
+    public TestDialogFragment() {
         // Public empty constructor used to handle lifecycle events.
     }
 
-    public static NightModeDialogFragment newInstance() {
-        return new NightModeDialogFragment();
+    public static TestDialogFragment newInstance() {
+        return new TestDialogFragment();
     }
 
     @NonNull
     @Override
     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
         AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
-        builder.setTitle("NightModeDialogFragment");
-        builder.setMessage("NightModeDialogFragment");
+        builder.setTitle("TestDialogFragment");
+        builder.setMessage("TestDialogFragment");
         return builder.create();
     }
 }
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt
new file mode 100644
index 0000000..976fac7
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesActivityTestRule.kt
@@ -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.appcompat.testutils
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.LocalesUpdateActivity
+import androidx.appcompat.testutils.LocalesUtils.initCustomLocaleList
+import androidx.core.os.LocaleListCompat
+
+@Suppress("DEPRECATION")
+class LocalesActivityTestRule<T : AppCompatActivity>(
+    activityClazz: Class<T>,
+    initialTouchMode: Boolean = false,
+    launchActivity: Boolean = true
+) : androidx.test.rule.ActivityTestRule<T>(activityClazz, initialTouchMode, launchActivity) {
+    override fun beforeActivityLaunched() {
+        initCustomLocaleList()
+        // By default we'll set the locales to match system locales, which allows us to make better
+        // assumptions in the test below.
+        runOnUiThread {
+            AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
+        }
+    }
+
+    override fun afterActivityFinished() {
+        // Reset locales persisted in static storage.
+        LocalesUpdateActivity.teardown()
+    }
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt
new file mode 100644
index 0000000..36dd7ea
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/testutils/LocalesUtils.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.appcompat.testutils
+
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import android.util.Log
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import androidx.lifecycle.Lifecycle
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.testutils.LifecycleOwnerUtils
+import androidx.testutils.PollingCheck
+import java.util.Locale
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+
+object LocalesUtils {
+    private const val LOG_TAG = "LocalesUtils"
+
+    /**
+     * A test {@link LocaleListCompat} containing locales [CANADA_FRENCH, CHINESE].
+     */
+    var CUSTOM_LOCALE_LIST: LocaleListCompat = LocaleListCompat.getEmptyLocaleList()
+
+    fun initCustomLocaleList() {
+        if (Build.VERSION.SDK_INT >= 24) {
+            CUSTOM_LOCALE_LIST = LocaleListCompat.forLanguageTags(
+                Locale.CANADA_FRENCH.toLanguageTag() + "," +
+                    Locale.CHINESE.toLanguageTag()
+            )
+        } else {
+            CUSTOM_LOCALE_LIST = LocaleListCompat.create(Locale.CHINESE)
+        }
+    }
+
+    fun assertConfigurationLocalesEquals(
+        expectedLocales: LocaleListCompat,
+        context: Context
+    ) {
+        assertConfigurationLocalesEquals(
+            null,
+            expectedLocales,
+            context
+        )
+    }
+
+    fun assertConfigurationLocalesEquals(
+        message: String?,
+        expectedLocales: LocaleListCompat,
+        context: Context
+    ) {
+        assertConfigurationLocalesEquals(
+            message,
+            expectedLocales,
+            context.resources.configuration
+        )
+    }
+
+    fun assertConfigurationLocalesEquals(
+        expectedLocales: LocaleListCompat,
+        configuration: Configuration
+    ) {
+        assertConfigurationLocalesEquals(
+            null,
+            expectedLocales,
+            configuration
+        )
+    }
+
+    fun assertConfigurationLocalesEquals(
+        message: String?,
+        expectedLocales: LocaleListCompat,
+        configuration: Configuration
+    ) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            assertEquals(
+                message,
+                expectedLocales.toLanguageTags(),
+                configuration.locales.toLanguageTags()
+            )
+        } else {
+            assertEquals(
+                message,
+                expectedLocales.get(0),
+                @Suppress("DEPRECATION") configuration.locale
+            )
+        }
+    }
+
+    fun <T : AppCompatActivity> setLocalesAndWait(
+        @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+        locales: LocaleListCompat
+    ) {
+        setLocalesAndWait(activityRule.activity, activityRule, locales)
+    }
+
+    fun <T : AppCompatActivity> setLocalesAndWait(
+        activity: AppCompatActivity?,
+        @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+        locales: LocaleListCompat
+    ) {
+        Log.d(
+            LOG_TAG,
+            "setLocalesAndWait on Activity: " + activity +
+                " to locales: " + locales
+        )
+
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        activityRule.runOnUiThread { setLocales(locales) }
+        instrumentation.waitForIdleSync()
+    }
+
+    fun <T : AppCompatActivity> setLocalesAndWaitForRecreate(
+        @Suppress("DEPRECATION") activityRule: androidx.test.rule.ActivityTestRule<T>,
+        locales: LocaleListCompat
+    ): T = setLocalesAndWaitForRecreate(activityRule.activity, locales)
+
+    fun <T : AppCompatActivity> setLocalesAndWaitForRecreate(
+        activity: T,
+        locales: LocaleListCompat
+    ): T {
+        Log.d(
+            LOG_TAG,
+            "setLocalesAndWaitForRecreate on Activity: " + activity +
+                " to mode: " + locales
+        )
+
+        LifecycleOwnerUtils.waitUntilState(activity, Lifecycle.State.RESUMED)
+
+        // Screen rotation kicks off a lot of background work, so we might need to wait a bit
+        // between the activity reaching RESUMED state and it actually being shown on screen.
+        PollingCheck.waitFor {
+            activity.hasWindowFocus()
+        }
+        assertNotEquals(locales, getLocales())
+
+        // Now perform locales change and wait for the Activity to be recreated.
+        return LifecycleOwnerUtils.waitForRecreation(activity) {
+            setLocales(locales)
+        }
+    }
+
+    fun setLocales(
+        locales: LocaleListCompat
+    ) = AppCompatDelegate.setApplicationLocales(locales)
+
+    private fun getLocales(): LocaleListCompat = AppCompatDelegate.getApplicationLocales()
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/res/layout/activity_locales.xml b/appcompat/appcompat/src/androidTest/res/layout/activity_locales.xml
new file mode 100644
index 0000000..cbbdf80
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/activity_locales.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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/text_locales"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="locales" />
+
+    <WebView
+        android:id="@+id/webView"
+        android:layout_width="match_parent"
+        android:layout_height="100dp"
+        android:layout_gravity="bottom" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
index 981c192..41c34c9 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatActivity.java
@@ -45,6 +45,7 @@
 import androidx.core.app.ActivityCompat;
 import androidx.core.app.NavUtils;
 import androidx.core.app.TaskStackBuilder;
+import androidx.core.os.LocaleListCompat;
 import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.ViewTreeLifecycleOwner;
 import androidx.lifecycle.ViewTreeViewModelStoreOwner;
@@ -662,4 +663,13 @@
      */
     protected void onNightModeChanged(@NightMode int mode) {
     }
+
+    /**
+     * Called when the locales have been changed. See {@link AppCompatDelegate#applyAppLocales()}
+     * for more information.
+     *
+     * @param locales the localeListCompat which has been applied
+     */
+    protected void onLocalesChanged(@NonNull LocaleListCompat locales) {
+    }
 }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index fc04398..1fc0287 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -17,12 +17,21 @@
 package androidx.appcompat.app;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.appcompat.app.AppLocalesStorageHelper.persistLocales;
+import static androidx.appcompat.app.AppLocalesStorageHelper.readLocales;
+import static androidx.appcompat.app.AppLocalesStorageHelper.syncLocalesToFramework;
+
+import static java.util.Objects.requireNonNull;
 
 import android.app.Activity;
 import android.app.Dialog;
+import android.app.LocaleManager;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.os.LocaleList;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MenuInflater;
@@ -30,19 +39,25 @@
 import android.view.ViewGroup;
 import android.view.Window;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
 import androidx.annotation.IdRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.view.ActionMode;
 import androidx.appcompat.widget.Toolbar;
 import androidx.appcompat.widget.VectorEnabledTintResources;
 import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
+import androidx.core.os.LocaleListCompat;
 import androidx.core.view.WindowCompat;
 import androidx.fragment.app.FragmentActivity;
 
@@ -95,6 +110,10 @@
     static final boolean DEBUG = false;
     static final String TAG = "AppCompatDelegate";
 
+    static AppLocalesStorageHelper.SerialExecutor sSerialExecutorForLocalesStorage = new
+            AppLocalesStorageHelper.SerialExecutor(
+                    new AppLocalesStorageHelper.ThreadPerTaskExecutor());
+
     /**
      * Mode which uses the system's night mode setting to determine if it is night or not.
      *
@@ -165,6 +184,13 @@
     @NightMode
     private static int sDefaultNightMode = MODE_NIGHT_UNSPECIFIED;
 
+    private static LocaleListCompat sRequestedAppLocales = null;
+    private static LocaleListCompat sStoredAppLocales = null;
+    private static Boolean sIsAutoStoreLocalesOptedIn = null;
+    private static boolean sIsFrameworkSyncChecked = false;
+    private static Object sLocaleManager = null;
+    private static Context sAppContext = null;
+
     /**
      * All AppCompatDelegate instances associated with a "live" Activity, e.g. lifecycle state is
      * post-onCreate and pre-onDestroy. These instances are used to instrument night mode's uiMode
@@ -173,6 +199,7 @@
     private static final ArraySet<WeakReference<AppCompatDelegate>> sActivityDelegates =
             new ArraySet<>();
     private static final Object sActivityDelegatesLock = new Object();
+    private static final Object sAppLocalesStorageSyncLock = new Object();
 
     /** @hide */
     @SuppressWarnings("deprecation")
@@ -521,6 +548,31 @@
     public abstract boolean applyDayNight();
 
     /**
+     * Applies the current locales to this delegate's host component.
+     *
+     * <p>Apps can be notified when the locales are changed by overriding the
+     * {@link AppCompatActivity#onLocalesChanged(LocaleListCompat)} method.</p>
+     *
+     * <p>This is a default implementation and it is overridden atin
+     * {@link AppCompatDelegateImpl#applyAppLocales()} </p>
+     *
+     * @see #setApplicationLocales(LocaleListCompat)
+     *
+     * @return true if requested app-specific locales were applied, false if not.
+     */
+    boolean applyAppLocales() {
+        return false;
+    }
+
+    /**
+     * Returns the context for the current delegate.
+     */
+    @Nullable
+    public Context getContextForDelegate() {
+        return null;
+    }
+
+    /**
      * Override the night mode used for this delegate's host component.
      *
      * <p>When setting a mode to be used across an entire app, the
@@ -595,6 +647,111 @@
     }
 
     /**
+     * Sets the current locales for the calling app.
+     *
+     * <p>If this method is called after any host components with attached
+     * {@link AppCompatDelegate}s have been 'created', a {@link LocaleList} configuration
+     * change will occur in each. This may result in those components being recreated, depending
+     * on their manifest configuration.</p>
+     *
+     * <p>This method accepts {@link LocaleListCompat} as an input parameter.</p>
+     *
+     * <p>Apps should continue to read Locales via their in-process {@link LocaleList}s.</p>
+     *
+     * <p>On API level 33 and above, this API will handle storage automatically.</p>
+     *
+     * <p>For API levels below that, the developer has two options:</p>
+     * <ul>
+     *     <li>They can opt-in to automatic storage handled through the library. They can do this by
+     *     adding a special metaData entry in their {@code AndroidManifest.xml}, similar to :
+     *     <pre><code>
+     *     &lt;service
+     *         android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+     *         android:enabled="false"
+     *         android:exported="false"&gt;
+     *         &lt;meta-data
+     *             android:name="autoStoreLocales"
+     *             android:value="true" /&gt;
+     *     &lt;/service&gt;
+     *     </code></pre>
+     *     They should be mindful that this will cause a blocking diskRead and diskWrite
+     *     strictMode violation, and they might need to suppress it at their end.</li>
+     *
+     *     <li>The second option is that they can choose to handle storage themselves. In order to
+     *     do so they must use this API to initialize locales during app-start up and provide
+     *     their stored locales. In this case, API should be called before Activity.onCreate()
+     *     in the activity lifecycle, e.g. in attachBaseContext().
+     *     <b>Note: Developers should gate this to API versions < 33.</b></li>
+     * </ul>
+     *
+     * <p>When the application using this API with API versions < 33 updates to a
+     * version >= 33, then there can be two scenarios for this transition:
+     * <ul>
+     *     <li>If the developer has opted-in for autoStorage then the locales will be automatically
+     *     synced to the framework. Developers must specify android:enabled="false" for the
+     *     AppLocalesMetadataHolderService as shown in the meta-data entry above.</li>
+     *     <li>If the developer has not opted-in for autoStorage then they will need to handle
+     *     this transition on their end.</li>
+     * </ul>
+     *
+     * @param locales a list of locales.
+     */
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+    public static void setApplicationLocales(@NonNull LocaleListCompat locales) {
+        requireNonNull(locales);
+        if (BuildCompat.isAtLeastT()) {
+            // If the API version is 33 (version for T) or above we want to redirect the call to
+            // the framework API.
+            Object localeManager = getLocaleManagerForApplication();
+            if (localeManager != null) {
+                Api33Impl.localeManagerSetApplicationLocales(localeManager,
+                        Api24Impl.localeListForLanguageTags(locales.toLanguageTags()));
+            }
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, String.format("sRequestedAppLocales. New:%s, Current:%s",
+                        locales, sRequestedAppLocales));
+            }
+            if (!locales.equals(sRequestedAppLocales)) {
+                synchronized (sActivityDelegatesLock) {
+                    sRequestedAppLocales = locales;
+                    applyLocalesToActiveDelegates();
+                }
+            } else if (DEBUG) {
+                Log.d(TAG, String.format("Not applying changes, sRequestedAppLocales is already %s",
+                        locales));
+            }
+        }
+    }
+
+    /**
+     * Returns application locales for the calling app as a {@link LocaleListCompat}.
+     *
+     * <p>Returns a {@link LocaleListCompat#getEmptyLocaleList()} if no app-specific locales are
+     * set.
+     */
+    @AnyThread
+    @NonNull
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+    public static LocaleListCompat getApplicationLocales() {
+        if (BuildCompat.isAtLeastT()) {
+            // If the API version is 33 or above we want to redirect the call to the framework API.
+            Object localeManager = getLocaleManagerForApplication();
+            if (localeManager != null) {
+                return LocaleListCompat.wrap(Api33Impl.localeManagerGetApplicationLocales(
+                        localeManager));
+            }
+        } else {
+            if (sRequestedAppLocales != null) {
+                // If app-specific locales exists then sRequestedApplicationLocales contains the
+                // latest locales.
+                return sRequestedAppLocales;
+            }
+        }
+        return LocaleListCompat.getEmptyLocaleList();
+    }
+
+    /**
      * Returns the default night mode.
      *
      * @see #setDefaultNightMode(int)
@@ -605,6 +762,183 @@
     }
 
     /**
+     * Returns the requested app locales.
+     *
+     * @see #setApplicationLocales(LocaleListCompat)
+     */
+    @Nullable
+    static LocaleListCompat getRequestedAppLocales() {
+        return sRequestedAppLocales;
+    }
+
+    /**
+     * Returns the stored app locales.
+     *
+     * @see #setApplicationLocales(LocaleListCompat)
+     */
+    @Nullable
+    static LocaleListCompat getStoredAppLocales() {
+        return sStoredAppLocales;
+    }
+
+    /**
+     * Resets the static variables for requested and stored locales to null. This method is used
+     * for testing as it mimics activity restart which is difficult to do in a test.
+     */
+    @VisibleForTesting
+    static void resetStaticRequestedAndStoredLocales() {
+        sRequestedAppLocales = null;
+        sStoredAppLocales = null;
+    }
+
+    /**
+     * Sets {@link AppCompatDelegate#sIsAutoStoreLocalesOptedIn} to the provided value. This method
+     * is used for testing, setting sIsAutoStoreLocalesOptedIn to true mimics adding an opt-in
+     * "autoStoreLocales" meta-data entry.
+     *
+     * see {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}.
+     */
+    @VisibleForTesting
+    static void setIsAutoStoreLocalesOptedIn(boolean isAutoStoreLocalesOptedIn) {
+        sIsAutoStoreLocalesOptedIn = isAutoStoreLocalesOptedIn;
+    }
+
+    /**
+     * Returns the localeManager for the current application using active delegates to fetch
+     * context, returns null if no active delegates present.
+     */
+    @RequiresApi(33)
+    static Object getLocaleManagerForApplication() {
+        if (sLocaleManager != null) {
+            return sLocaleManager;
+        }
+        // Traversing through the active delegates to retrieve context for any one non null
+        // delegate.
+        // This context is used to create a localeManager which is saved as a static variable to
+        // reduce multiple object creation for different activities.
+        if (sAppContext == null) {
+            for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {
+                final AppCompatDelegate delegate = activeDelegate.get();
+                if (delegate != null) {
+                    Context context = delegate.getContextForDelegate();
+                    if (context != null) {
+                        sAppContext = context;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (sAppContext != null) {
+            sLocaleManager = sAppContext.getSystemService(Context.LOCALE_SERVICE);
+        }
+        return sLocaleManager;
+    }
+
+    /**
+     * Returns true is the "autoStoreLocales" metaData is marked true in the app manifest.
+     */
+    static boolean isAutoStorageOptedIn(Context context) {
+        if (sIsAutoStoreLocalesOptedIn == null) {
+            try {
+                ServiceInfo serviceInfo = AppLocalesMetadataHolderService.getServiceInfo(
+                        context);
+                if (serviceInfo.metaData != null) {
+                    sIsAutoStoreLocalesOptedIn = serviceInfo.metaData.getBoolean(
+                            /* key= */ "autoStoreLocales");
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.d(TAG, "Checking for metadata for AppLocalesMetadataHolderService "
+                        + ": Service not found");
+                sIsAutoStoreLocalesOptedIn = false;
+            }
+        }
+        return sIsAutoStoreLocalesOptedIn;
+    }
+
+    /**
+     * Executes {@link AppCompatDelegate#syncRequestedAndStoredLocales(Context)} asynchronously
+     * on a worker thread, serialized using {@link
+     * AppCompatDelegate#sSerialExecutorForLocalesStorage}.
+     *
+     * <p>This is done to perform the storage read operation without blocking the main thread.</p>
+     */
+    void asyncExecuteSyncRequestedAndStoredLocales(Context context) {
+        sSerialExecutorForLocalesStorage.execute(() -> syncRequestedAndStoredLocales(context));
+    }
+
+    /**
+     * Syncs requested and persisted app-specific locales.
+     *
+     * <p>This sync is only performed if the developer has opted in to use the autoStoredLocales
+     * feature, marked by the metaData "autoStoreLocales" wrapped in the service
+     * "AppLocalesMetadataHolderService". If the metaData is not found in the manifest or holds
+     * the value false then we return from this function without doing anything. If the metaData
+     * is set to true, then we perform a sync for app-locales.</p>
+     *
+     * <p>If the API version is >=33, then the storage is checked for app-specific locales, if
+     * found they are synced to the framework by calling the
+     * {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}</p>
+     *
+     * <p>If the API version is <33, then there are two scenarios:</p>
+     * <ul>
+     * <li>If the requestedAppLocales are not set then the app-specific locales are read from
+     * storage. If persisted app-specific locales are found then they are used to
+     * update the requestedAppLocales.</li>
+     * <li>If the requestedAppLocales are populated and are different from the stored locales
+     * then in that case the requestedAppLocales are stored and the static variable for
+     * storedAppLocales is updated accordingly.</li>
+     * </ul>
+     */
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+    static void syncRequestedAndStoredLocales(Context context) {
+        if (!isAutoStorageOptedIn(context)) {
+            return;
+        } else if (BuildCompat.isAtLeastT()) {
+            // TODO: After BuildCompat.isAtLeast() is deprecated, the above condition needs to be
+            //  replaced by (Build.VERSION.SDK_INT == 33).
+            if (!sIsFrameworkSyncChecked) {
+                // syncs locales from androidX to framework, it only happens once after the
+                // device is updated to T (API version 33).
+                sSerialExecutorForLocalesStorage.execute(() -> {
+                    syncLocalesToFramework(context);
+                    sIsFrameworkSyncChecked = true;
+                });
+            }
+        } else {
+            synchronized (sAppLocalesStorageSyncLock) {
+                if (sRequestedAppLocales == null) {
+                    if (sStoredAppLocales == null) {
+                        sStoredAppLocales =
+                                LocaleListCompat.forLanguageTags(readLocales(context));
+                    }
+                    if (sStoredAppLocales.isEmpty()) {
+                        // if both requestedLocales and storedLocales not set, then the user has not
+                        // specified any application-specific locales. So no alterations in current
+                        // application locales should take place.
+                        return;
+                    }
+                    sRequestedAppLocales = sStoredAppLocales;
+                } else if (!sRequestedAppLocales.equals(sStoredAppLocales)) {
+                    // if requestedLocales is set and is not equal to the storedLocales then in this
+                    // case we need to store these locales in storage.
+                    sStoredAppLocales = sRequestedAppLocales;
+                    persistLocales(context, sRequestedAppLocales.toLanguageTags());
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Sets the value for {@link AppCompatDelegate#sAppContext} which is the context for the
+     * current application.
+     */
+    static void setAppContext(Context context) {
+        sAppContext = context;
+    }
+
+    /**
      * Sets whether vector drawables on older platforms (< API 21) can be used within
      * {@link android.graphics.drawable.DrawableContainer} resources.
      *
@@ -695,4 +1029,48 @@
             }
         }
     }
+
+    private static void applyLocalesToActiveDelegates() {
+        for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {
+            final AppCompatDelegate delegate = activeDelegate.get();
+            if (delegate != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "applyLocalesToActiveDelegates. Applying to " + delegate);
+                }
+                delegate.applyAppLocales();
+            }
+        }
+    }
+
+    @RequiresApi(24)
+    static class Api24Impl {
+        private Api24Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static LocaleList localeListForLanguageTags(String list) {
+            return LocaleList.forLanguageTags(list);
+        }
+    }
+
+    @RequiresApi(33)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void localeManagerSetApplicationLocales(Object localeManager,
+                LocaleList locales) {
+            LocaleManager mLocaleManager = (LocaleManager) localeManager;
+            mLocaleManager.setApplicationLocales(locales);
+        }
+
+        @DoNotInline
+        static LocaleList localeManagerGetApplicationLocales(Object localeManager) {
+            LocaleManager mLocaleManager = (LocaleManager) localeManager;
+            return mLocaleManager.getApplicationLocales();
+        }
+    }
 }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index f42cc0b..bcdc36b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -24,6 +24,7 @@
 import static android.view.Window.FEATURE_OPTIONS_PANEL;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.appcompat.app.LocaleOverlayHelper.combineLocalesIfOverlayExists;
 
 import android.annotation.SuppressLint;
 import android.app.Activity;
@@ -75,6 +76,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -109,6 +111,7 @@
 import androidx.core.app.NavUtils;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.LocaleListCompat;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.view.KeyEventDispatcher;
 import androidx.core.view.LayoutInflaterCompat;
@@ -125,6 +128,7 @@
 import org.xmlpull.v1.XmlPullParser;
 
 import java.util.List;
+import java.util.Locale;
 
 /**
  * @hide
@@ -258,9 +262,8 @@
     private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED;
 
     private int mThemeResId;
-    private boolean mActivityHandlesUiMode;
-    private boolean mActivityHandlesUiModeChecked;
-
+    private int mActivityHandlesConfigFlags;
+    private boolean mActivityHandlesConfigFlagsChecked;
     private AutoNightModeManager mAutoTimeNightModeManager;
     private AutoNightModeManager mAutoBatteryNightModeManager;
 
@@ -360,13 +363,22 @@
 
         final int modeToApply = mapNightMode(baseContext, calculateNightMode());
 
+        if (isAutoStorageOptedIn(baseContext)) {
+            // If the developer has opted in to auto store the locales, then we use
+            // syncRequestedAndStoredLocales() to load the saved locales from storage. This is
+            // performed only during cold app start-ups because in other cases the locales can be
+            // found in the static storage.
+            syncRequestedAndStoredLocales(baseContext);
+        }
+        final LocaleListCompat localesToApply = calculateApplicationLocales(baseContext);
+
         // If the base context is a ContextThemeWrapper (thus not an Application context)
         // and nobody's touched its Resources yet, we can shortcut and directly apply our
         // override configuration.
         if (sCanApplyOverrideConfiguration
                 && baseContext instanceof android.view.ContextThemeWrapper) {
-            final Configuration config = createOverrideConfigurationForDayNight(
-                    baseContext, modeToApply, null);
+            final Configuration config = createOverrideAppConfiguration(
+                    baseContext, modeToApply, localesToApply, null);
             if (DEBUG) {
                 Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                         config.toString()));
@@ -385,8 +397,8 @@
 
         // Again, but using the AppCompat version of ContextThemeWrapper.
         if (baseContext instanceof ContextThemeWrapper) {
-            final Configuration config = createOverrideConfigurationForDayNight(
-                    baseContext, modeToApply, null);
+            final Configuration config = createOverrideAppConfiguration(
+                    baseContext, modeToApply, localesToApply, null);
             if (DEBUG) {
                 Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                         config.toString()));
@@ -442,8 +454,8 @@
             }
         }
 
-        final Configuration config = createOverrideConfigurationForDayNight(
-                baseContext, modeToApply, configOverlay);
+        final Configuration config = createOverrideAppConfiguration(
+                baseContext, modeToApply, localesToApply, configOverlay);
         if (DEBUG) {
             Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
                     + "applyOverrideConfiguration(). Config: %s", config.toString()));
@@ -497,9 +509,9 @@
         // Dialogs, etc
         mBaseContextAttached = true;
 
-        // Our implicit call to applyDayNight() should not recreate until after the Activity is
-        // created
-        applyDayNight(false);
+        // Our implicit call to applyApplicationSpecificConfig() should not recreate
+        // until after the Activity is created
+        applyApplicationSpecificConfig(false);
 
         // We lazily fetch the Window for Activities, to allow DayNight to apply in
         // attachBaseContext
@@ -661,16 +673,18 @@
         // inspects the last-seen configuration. Otherwise, we'll recurse back to this method.
         mEffectiveConfiguration = new Configuration(mContext.getResources().getConfiguration());
 
-        // Re-apply Day/Night with the new configuration but disable recreations. Since this
-        // configuration change has only just happened we can safely just update the resources now
-        applyDayNight(false);
+        // Re-apply Day/Night and locales with the new configuration but disable recreations.
+        // Since this configuration change has only just happened we can safely just update the
+        // resources now.
+        applyApplicationSpecificConfig(false);
     }
 
     @Override
     public void onStart() {
         // This will apply day/night if the time has changed, it will also call through to
-        // setupAutoNightModeIfNeeded()
-        applyDayNight();
+        // setupAutoNightModeIfNeeded(). Also, this will make sure that the latest
+        // app-specific locales are applied and activity is recreated if needed.
+        applyApplicationSpecificConfig(true);
     }
 
     @Override
@@ -2369,15 +2383,35 @@
     }
 
     @Override
+    public Context getContextForDelegate() {
+        return mContext;
+    }
+
+    @Override
     public boolean applyDayNight() {
-        return applyDayNight(true);
+        return applyApplicationSpecificConfig(true);
+    }
+
+    @Override
+    boolean applyAppLocales() {
+        // This method is only reached when there is an explicit call to setApplicationLocales().
+        if (isAutoStorageOptedIn(mContext)
+                && getRequestedAppLocales() != null
+                && !getRequestedAppLocales().equals(getStoredAppLocales())) {
+            // If the developer has opted in to autoStore the locales, we need to store the locales
+            // for the application here. This is done using the syncRequestedAndStoredLocales,
+            // called asynchronously on a worker thread.
+            asyncExecuteSyncRequestedAndStoredLocales(mContext);
+        }
+        return applyApplicationSpecificConfig(true);
     }
 
     @SuppressWarnings("deprecation")
-    private boolean applyDayNight(final boolean allowRecreation) {
+    private boolean applyApplicationSpecificConfig(final boolean allowRecreation) {
         if (mDestroyed) {
             if (DEBUG) {
-                Log.d(TAG, "applyDayNight. Skipping because host is destroyed");
+                Log.d(TAG, "applyApplicationSpecificConfig. Skipping because host is "
+                        + "destroyed");
             }
             // If we're destroyed, ignore the call
             return false;
@@ -2385,7 +2419,14 @@
 
         @NightMode final int nightMode = calculateNightMode();
         @ApplyableNightMode final int modeToApply = mapNightMode(mContext, nightMode);
-        final boolean applied = updateForNightMode(modeToApply, allowRecreation);
+
+        LocaleListCompat localesToBeApplied = null;
+        if (Build.VERSION.SDK_INT < 33) {
+            localesToBeApplied = calculateApplicationLocales(mContext);
+        }
+
+        final boolean applied = updateAppConfiguration(modeToApply, localesToBeApplied,
+                allowRecreation);
 
         if (nightMode == MODE_NIGHT_AUTO_TIME) {
             getAutoTimeNightModeManager(mContext).setup();
@@ -2403,6 +2444,50 @@
         return applied;
     }
 
+    /**
+     * Returns the required {@link LocaleListCompat}  for the current application. This method
+     * checks for requested app-specific locales and returns them after an overlay
+     * with the system locales. If requested app-specific do not exist, it returns a null.
+     */
+    @Nullable
+    LocaleListCompat calculateApplicationLocales(@NonNull Context context) {
+        if (Build.VERSION.SDK_INT >= 33) {
+            return null;
+        }
+        LocaleListCompat requestedLocales = getRequestedAppLocales();
+        if (requestedLocales == null) {
+            return null;
+        }
+        LocaleListCompat systemLocales = getConfigurationLocales(
+                context.getApplicationContext()
+                        .getResources().getConfiguration());
+
+        LocaleListCompat localesToBeApplied;
+        if (Build.VERSION.SDK_INT >= 24) {
+            // For API>=24 the application locales are applied as a localeList. The localeList
+            // to be applied is an overlay of app-specific locales and the system locales.
+            localesToBeApplied = combineLocalesIfOverlayExists(requestedLocales,
+                    systemLocales);
+        } else {
+            // For API<24 the application does not have a localeList instead it has a single
+            // locale, which we have set as the locale with the highest preference i.e. the first
+            // one from the requested locales.
+            if (requestedLocales.isEmpty()) {
+                localesToBeApplied = LocaleListCompat.getEmptyLocaleList();
+            } else {
+                localesToBeApplied =
+                        LocaleListCompat.forLanguageTags(requestedLocales.get(0).toString());
+            }
+        }
+
+        if (localesToBeApplied.isEmpty()) {
+            // If the localesToBeApplied is empty, it implies that there are no app-specific locales
+            // set for this application and systemLocales should be followed.
+            localesToBeApplied = systemLocales;
+        }
+        return localesToBeApplied;
+    }
+
     @Override
     @RequiresApi(17)
     public void setLocalNightMode(@NightMode int mode) {
@@ -2461,9 +2546,38 @@
         return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode();
     }
 
+    void setConfigurationLocales(Configuration conf, @NonNull LocaleListCompat locales) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            Api24Impl.setLocales(conf, locales);
+        } else if (Build.VERSION.SDK_INT >= 17) {
+            Api17Impl.setLocale(conf, locales.get(0));
+            Api17Impl.setLayoutDirection(conf, locales.get(0));
+        } else {
+            conf.locale = locales.get(0);
+        }
+    }
+
+    LocaleListCompat getConfigurationLocales(Configuration conf) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return Api24Impl.getLocales(conf);
+        } else if (Build.VERSION.SDK_INT >= 21) {
+            return LocaleListCompat.forLanguageTags(Api21Impl.toLanguageTag(conf.locale));
+        } else {
+            return LocaleListCompat.create(conf.locale);
+        }
+    }
+
+    void setDefaultLocalesForLocaleList(LocaleListCompat locales) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            Api24Impl.setDefaultLocales(locales);
+        } else {
+            Locale.setDefault(locales.get(0));
+        }
+    }
+
     @NonNull
-    private Configuration createOverrideConfigurationForDayNight(
-            @NonNull Context context, @ApplyableNightMode final int mode,
+    private Configuration createOverrideAppConfiguration(@NonNull Context context,
+            @ApplyableNightMode int mode, @Nullable LocaleListCompat locales,
             @Nullable Configuration configOverlay) {
         int newNightMode;
         switch (mode) {
@@ -2492,43 +2606,76 @@
         overrideConf.uiMode = newNightMode
                 | (overrideConf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
 
+        if (locales != null) {
+            setConfigurationLocales(overrideConf, locales);
+        }
         return overrideConf;
     }
 
     /**
-     * Updates the {@link Resources} configuration {@code uiMode} with the
-     * chosen {@code UI_MODE_NIGHT} value.
+     * Updates the {@link Resources} configuration {@code uiMode}  and {@Link LocaleList} with the
+     * chosen configuration values.
      *
-     * @param mode The new night mode to apply
+     * @param nightMode The new night mode to apply
+     * @param locales The new Locales to be applied
      * @param allowRecreation whether to attempt activity recreate
      * @return true if an action has been taken (recreation, resources updating, etc)
      */
-    private boolean updateForNightMode(@ApplyableNightMode final int mode,
-            final boolean allowRecreation) {
+    private boolean updateAppConfiguration(int nightMode, @Nullable LocaleListCompat
+            locales, final boolean allowRecreation) {
         boolean handled = false;
 
         final Configuration overrideConfig =
-                createOverrideConfigurationForDayNight(mContext, mode, null);
+                createOverrideAppConfiguration(mContext, nightMode, locales, null);
 
-        final boolean activityHandlingUiMode = isActivityManifestHandlingUiMode();
+        final int activityHandlingConfigChange = getActivityHandlesConfigChangesFlags();
         final Configuration currentConfiguration = mEffectiveConfiguration == null
                 ? mContext.getResources().getConfiguration() : mEffectiveConfiguration;
         final int currentNightMode = currentConfiguration.uiMode
                 & Configuration.UI_MODE_NIGHT_MASK;
         final int newNightMode = overrideConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
 
-        if (DEBUG) {
-            Log.d(TAG, String.format(
-                    "updateForNightMode [allowRecreation:%s, currentNightMode:%d, "
-                            + "newNightMode:%d, activityHandlingUiMode:%s, baseContextAttached:%s, "
-                            + "created:%s, canReturnDifferentContext:%s, host:%s]",
-                    allowRecreation, currentNightMode, newNightMode, activityHandlingUiMode,
-                    mBaseContextAttached, mCreated, sCanReturnDifferentContext, mHost));
+        final LocaleListCompat currentLocales = getConfigurationLocales(currentConfiguration);
+        final LocaleListCompat newLocales;
+        if (locales == null) {
+            newLocales = null;
+        } else {
+            newLocales = getConfigurationLocales(overrideConfig);
         }
 
-        if (currentNightMode != newNightMode
+        // Bitmask representing if there is a change in nightMode or Locales, mapped by bits
+        // ActivityInfo.CONFIG_UI_MODE and ActivityInfo.CONFIG_LOCALE respectively.
+        int configChanges = 0;
+        if (currentNightMode != newNightMode) {
+            configChanges |= ActivityInfo.CONFIG_UI_MODE;
+        }
+        if (newLocales != null && !currentLocales.equals(newLocales)) {
+            configChanges |= ActivityInfo.CONFIG_LOCALE;
+            if (Build.VERSION.SDK_INT >= 17) {
+                configChanges |= ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+            }
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, String.format(
+                    "updateAppConfiguration [allowRecreation:%s, "
+                            + "currentNightMode:%s, newNightMode:%s, currentLocales:%s, "
+                            + "newLocales:%s, activityHandlingNightModeChanges:%s, "
+                            + "activityHandlingLocalesChanges:%s, "
+                            + "activityHandlingLayoutDirectionChanges:%s, "
+                            + "baseContextAttached:%s, "
+                            + "created:%s, canReturnDifferentContext:%s, host:%s]",
+                    allowRecreation, currentNightMode, newNightMode,
+                    currentLocales,
+                    newLocales,
+                    ((activityHandlingConfigChange & ActivityInfo.CONFIG_UI_MODE) != 0),
+                    ((activityHandlingConfigChange & ActivityInfo.CONFIG_LOCALE) != 0),
+                    ((activityHandlingConfigChange & ActivityInfo.CONFIG_LAYOUT_DIRECTION) != 0),
+                    mBaseContextAttached, mCreated,
+                    sCanReturnDifferentContext, mHost));
+        }
+        if ((~activityHandlingConfigChange & configChanges) != 0
                 && allowRecreation
-                && !activityHandlingUiMode
                 && mBaseContextAttached
                 && (sCanReturnDifferentContext || mCreated)
                 && mHost instanceof Activity
@@ -2537,41 +2684,56 @@
             // attachBaseContext() + createConfigurationContext() code path.
             // Else, we need to use updateConfiguration() before we're 'created' (below)
             if (DEBUG) {
-                Log.d(TAG, "updateForNightMode attempting to recreate Activity: " + mHost);
+                Log.d(TAG, "updateAppConfiguration attempting to recreate Activity: "
+                        + mHost);
             }
             ActivityCompat.recreate((Activity) mHost);
             handled = true;
         } else if (DEBUG) {
-            Log.d(TAG, "updateForNightMode not recreating Activity: " + mHost);
+            Log.d(TAG, "updateAppConfiguration not recreating Activity: " + mHost);
         }
 
-        if (!handled && currentNightMode != newNightMode) {
+        if (!handled && (configChanges != 0)) {
             // Else we need to use the updateConfiguration path
             if (DEBUG) {
-                Log.d(TAG, "updateForNightMode. Updating resources config on host: " + mHost);
+                Log.d(TAG, "updateAppConfiguration. Updating resources config on host: "
+                        + mHost);
             }
-            updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode, null);
+            // If all the configurations that need to be altered are handled by the activity,
+            // only then callOnConfigChange is set to true.
+            updateResourcesConfiguration(newNightMode, newLocales,
+                    /* callOnConfigChange = */(configChanges & activityHandlingConfigChange)
+                            == configChanges, null);
+
             handled = true;
         }
 
         if (DEBUG && !handled) {
-            Log.d(TAG, "updateForNightMode. Skipping. Night mode: " + mode + " for host:" + mHost);
+            Log.d(TAG,
+                    "updateAppConfiguration. Skipping. nightMode: " + nightMode + " and "
+                            + "locales: " +  locales + " for host:" + mHost);
         }
 
-        // Notify the activity of the night mode. We only notify if we handled the change,
-        // or the Activity is set to handle uiMode changes
         if (handled && mHost instanceof AppCompatActivity) {
-            ((AppCompatActivity) mHost).onNightModeChanged(mode);
+            if ((configChanges & ActivityInfo.CONFIG_UI_MODE) != 0) {
+                ((AppCompatActivity) mHost).onNightModeChanged(nightMode);
+            }
+            if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
+                ((AppCompatActivity) mHost).onLocalesChanged(locales);
+            }
         }
 
+        if (handled && newLocales != null) {
+            setDefaultLocalesForLocaleList(newLocales);
+        }
         return handled;
     }
 
-    private void updateResourcesConfigurationForNightMode(
-            final int uiModeNightModeValue, final boolean callOnConfigChange,
+    private void updateResourcesConfiguration(int uiModeNightModeValue,
+            @Nullable final LocaleListCompat locales, final boolean callOnConfigChange,
             @Nullable Configuration configOverlay) {
-        // If the Activity is not set to handle uiMode config changes we will
-        // update the Resources with a new Configuration with an updated UI Mode
+        // If the Activity is not set to handle config changes we will
+        // update the Resources with a new Configuration with  updated nightMode and locales.
         final Resources res = mContext.getResources();
         final Configuration conf = new Configuration(res.getConfiguration());
         if (configOverlay != null) {
@@ -2579,6 +2741,9 @@
         }
         conf.uiMode = uiModeNightModeValue
                 | (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
+        if (locales != null) {
+            setConfigurationLocales(conf, locales);
+        }
         res.updateConfiguration(conf, null);
 
         // We may need to flush the Resources' drawable cache due to framework bugs.
@@ -2602,19 +2767,23 @@
         }
 
         if (callOnConfigChange && mHost instanceof Activity) {
-            final Activity activity = (Activity) mHost;
-            if (activity instanceof LifecycleOwner) {
-                // If the Activity is a LifecyleOwner, check that it is after onCreate() and
-                // before onDestroy(), which includes STOPPED.
-                Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
-                if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) {
-                    activity.onConfigurationChanged(conf);
-                }
-            } else {
-                // Otherwise, we'll fallback to our internal created and destroyed flags.
-                if (mCreated && !mDestroyed) {
-                    activity.onConfigurationChanged(conf);
-                }
+            updateActivityConfiguration(conf);
+        }
+    }
+
+    private void updateActivityConfiguration(Configuration conf) {
+        final Activity activity = (Activity) mHost;
+        if (activity instanceof LifecycleOwner) {
+            // If the Activity is a LifecyleOwner, check that it is after onCreate() and
+            // before onDestroy(), which includes STOPPED.
+            Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
+            if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED)) {
+                activity.onConfigurationChanged(conf);
+            }
+        } else {
+            // Otherwise, we'll fallback to our internal created and destroyed flags.
+            if (mCreated && !mDestroyed) {
+                activity.onConfigurationChanged(conf);
             }
         }
     }
@@ -2644,13 +2813,14 @@
         return mAutoBatteryNightModeManager;
     }
 
-    private boolean isActivityManifestHandlingUiMode() {
-        if (!mActivityHandlesUiModeChecked && mHost instanceof Activity) {
+    private int getActivityHandlesConfigChangesFlags() {
+        if (!mActivityHandlesConfigFlagsChecked
+                && mHost instanceof Activity) {
             final PackageManager pm = mContext.getPackageManager();
             if (pm == null) {
-                // If we don't have a PackageManager, return false. Don't set
+                // If we don't have a PackageManager, return 0. Don't set
                 // the checked flag though so we still check again later
-                return false;
+                return 0;
             }
             try {
                 int flags = 0;
@@ -2667,19 +2837,19 @@
                 }
                 final ActivityInfo info = pm.getActivityInfo(
                         new ComponentName(mContext, mHost.getClass()), flags);
-                mActivityHandlesUiMode = info != null
-                        && (info.configChanges & ActivityInfo.CONFIG_UI_MODE) != 0;
+                if (info != null) {
+                    mActivityHandlesConfigFlags = info.configChanges;
+                }
             } catch (PackageManager.NameNotFoundException e) {
                 // This shouldn't happen but let's not crash because of it, we'll just log and
                 // return false (since most apps won't be handling it)
                 Log.d(TAG, "Exception while getting ActivityInfo", e);
-                mActivityHandlesUiMode = false;
+                mActivityHandlesConfigFlags = 0;
             }
         }
         // Flip the checked flag so we don't check again
-        mActivityHandlesUiModeChecked = true;
-
-        return mActivityHandlesUiMode;
+        mActivityHandlesConfigFlagsChecked = true;
+        return mActivityHandlesConfigFlags;
     }
 
     /**
@@ -3603,6 +3773,16 @@
                 @NonNull Configuration overrideConfiguration) {
             return context.createConfigurationContext(overrideConfiguration);
         }
+
+        @DoNotInline
+        static void setLayoutDirection(Configuration configuration, Locale loc) {
+            configuration.setLayoutDirection(loc);
+        }
+
+        @DoNotInline
+        static void setLocale(Configuration configuration, Locale loc) {
+            configuration.setLocale(loc);
+        }
     }
 
     @RequiresApi(21)
@@ -3612,12 +3792,20 @@
         static boolean isPowerSaveMode(PowerManager powerManager) {
             return powerManager.isPowerSaveMode();
         }
+
+        @DoNotInline
+        static String toLanguageTag(Locale locale) {
+            return locale.toLanguageTag();
+        }
     }
 
     @RequiresApi(24)
     static class Api24Impl {
         private Api24Impl() { }
 
+        // Most methods of LocaleListCompat requires a minimum API of 24 to be used and these are
+        // the helper implementations of those methods, used to indirectly invoke them in our code.
+        @DoNotInline
         static void generateConfigDelta_locale(@NonNull Configuration base,
                 @NonNull Configuration change, @NonNull Configuration delta) {
             final LocaleList baseLocales = base.getLocales();
@@ -3627,6 +3815,21 @@
                 delta.locale = change.locale;
             }
         }
+
+        @DoNotInline
+        static LocaleListCompat getLocales(Configuration configuration) {
+            return LocaleListCompat.forLanguageTags(configuration.getLocales().toLanguageTags());
+        }
+
+        @DoNotInline
+        static void setLocales(Configuration configuration, LocaleListCompat locales) {
+            configuration.setLocales(LocaleList.forLanguageTags(locales.toLanguageTags()));
+        }
+
+        @DoNotInline
+        public static void setDefaultLocales(LocaleListCompat locales) {
+            LocaleList.setDefault(LocaleList.forLanguageTags(locales.toLanguageTags()));
+        }
     }
 
     @RequiresApi(26)
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java
new file mode 100644
index 0000000..3fe13bc
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesMetadataHolderService.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.appcompat.app;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.LocaleListCompat;
+
+/**
+ * A placeholder service to avoid adding application-level metadata. The service
+ * is only used to expose metadata defined in the library's manifest. It is
+ * never invoked.
+ *
+ * <p>This metadata boolean collected from the key "autoStoreLocales" is used as an opt-in from
+ * the developers to automatically store locales provided by them through the API
+ * {@link AppCompatDelegate#setApplicationLocales(LocaleListCompat)}.</p>
+ */
+public final class AppLocalesMetadataHolderService extends Service {
+    public AppLocalesMetadataHolderService() {}
+
+    @NonNull
+    @Override
+    public IBinder onBind(@SuppressWarnings("InvalidNullability") @NonNull Intent intent) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns the {@link ServiceInfo} for the declared {@link AppLocalesMetadataHolderService}.
+     *
+     * <p>This serviceInfo contains the attribute "autoStoreLocales", its value being a boolean
+     * that informs us if the developer wants us to handle the storage of locales or not.</p>
+     */
+    @NonNull
+    @SuppressWarnings("deprecation") // GET_DISABLED_COMPONENTS, getServiceInfo
+    public static ServiceInfo getServiceInfo(@NonNull Context context) throws
+            PackageManager.NameNotFoundException {
+        int flags = PackageManager.GET_META_DATA;
+        // The service is marked as disabled so we need to include the following flags.
+        if (Build.VERSION.SDK_INT >= 24) {
+            flags |= Api24Impl.getDisabledComponentFlag();
+        } else {
+            flags |= PackageManager.GET_DISABLED_COMPONENTS;
+        }
+
+        return context.getPackageManager().getServiceInfo(
+                new ComponentName(context, AppLocalesMetadataHolderService.class), flags);
+    }
+
+    @RequiresApi(24)
+    private static class Api24Impl {
+        @DoNotInline
+        static int getDisabledComponentFlag() {
+            return PackageManager.MATCH_DISABLED_COMPONENTS;
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
new file mode 100644
index 0000000..d30f599
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.appcompat.app;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+
+import static androidx.appcompat.app.AppCompatDelegate.getApplicationLocales;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.LocaleListCompat;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class to manage storage of locales in app's persistent files.
+ */
+class AppLocalesStorageHelper {
+    static final String APPLICATION_LOCALES_RECORD_FILE =
+            "androidx.appcompat.app.AppCompatDelegate.application_locales_record_file";
+    static final String LOCALE_RECORD_ATTRIBUTE_TAG = "application_locales";
+    static final String LOCALE_RECORD_FILE_TAG = "locales";
+    static final String APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME = "androidx.appcompat.app"
+            + ".AppLocalesMetadataHolderService";
+    static final String TAG = "AppLocalesStorageHelper";
+
+    private AppLocalesStorageHelper() {}
+
+    /**
+     * Returns app locales after reading from storage, fetched using the application context.
+     */
+    @NonNull
+    static String readLocales(@NonNull Context context) {
+        String appLocales = "";
+
+        FileInputStream fis;
+        try {
+            fis = context.openFileInput(APPLICATION_LOCALES_RECORD_FILE);
+        } catch (FileNotFoundException fnfe) {
+            Log.w(TAG, "Reading app Locales : Locales record file not found: "
+                    + APPLICATION_LOCALES_RECORD_FILE);
+            return appLocales;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, "UTF-8");
+            int type;
+            int outerDepth = parser.getDepth();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+
+                String tagName = parser.getName();
+                if (tagName.equals(LOCALE_RECORD_FILE_TAG)) {
+                    appLocales =  parser.getAttributeValue(/*namespace= */ null,
+                            LOCALE_RECORD_ATTRIBUTE_TAG);
+                    break;
+                }
+            }
+        } catch (XmlPullParserException | IOException e) {
+            Log.w(TAG,
+                    "Reading app Locales : Unable to parse through file :"
+                            + APPLICATION_LOCALES_RECORD_FILE);
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    /* ignore */
+                }
+            }
+        }
+
+        if (!appLocales.isEmpty()) {
+            Log.d(TAG,
+                    "Reading app Locales : Locales read from file: "
+                            + APPLICATION_LOCALES_RECORD_FILE + " ," + " appLocales: "
+                            + appLocales);
+        } else {
+            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+        }
+        return appLocales;
+    }
+
+    /**
+     * Stores the provided locales in internal app file, using the application context.
+     */
+    static void persistLocales(@NonNull Context context, @NonNull String locales) {
+        if (locales.equals("")) {
+            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+            return;
+        }
+
+        FileOutputStream fos;
+        try {
+            fos = context.openFileOutput(APPLICATION_LOCALES_RECORD_FILE, Context.MODE_PRIVATE);
+        } catch (FileNotFoundException fnfe) {
+            Log.w(TAG, String.format("Storing App Locales : FileNotFoundException: Cannot open "
+                    + "file %s for writing ", APPLICATION_LOCALES_RECORD_FILE));
+            return;
+        }
+        XmlSerializer serializer = Xml.newSerializer();
+        try {
+            serializer.setOutput(fos, /* encoding= */ null);
+            serializer.startDocument("UTF-8", true);
+            serializer.startTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+            serializer.attribute(/* namespace= */ null, LOCALE_RECORD_ATTRIBUTE_TAG, locales);
+            serializer.endTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+            serializer.endDocument();
+            Log.d(TAG, "Storing App Locales : app-locales: "
+                    + locales + " persisted successfully.");
+        } catch (Exception e) {
+            Log.w(TAG, "Storing App Locales : Failed to persist app-locales: "
+                    + locales, e);
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    /* ignore */
+                }
+            }
+        }
+    }
+
+    /**
+     * Syncs app-specific locales from androidX to framework. This is used to maintain a smooth
+     * transition for a device that updates from pre-T API versions to T.
+     *
+     * <p><b>NOTE:</b> This should only be called when auto-storage is opted-in. This method
+     * uses the meta-data service provided during the opt-in and hence if the service is not found
+     * this method will throw an error.</p>
+     */
+    static void syncLocalesToFramework(Context context) {
+        ComponentName app_locales_component = new ComponentName(
+                context, APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME);
+
+        if (context.getPackageManager().getComponentEnabledSetting(app_locales_component)
+                != COMPONENT_ENABLED_STATE_ENABLED) {
+            AppCompatDelegate.setAppContext(context);
+            // ComponentEnabledSetting for the app component app_locales_component is used as a
+            // marker to represent that the locales has been synced from AndroidX to framework
+            // If this marker is found in ENABLED state then we do not need to sync again.
+            if (getApplicationLocales().isEmpty()) {
+                // We check if some locales are applied by the framework or not (this is done to
+                // ensure that we don't overwrite newer locales set by the framework). If no
+                // app-locales are found then we need to sync the app-specific locales from androidX
+                // to framework.
+
+                String appLocales = readLocales(context);
+                // if locales are present in storage, call the setApplicationLocales() API. As the
+                // API version is >= 33, this call will be directed to the framework API and the
+                // locales will be persisted there.
+                AppCompatDelegate.setApplicationLocales(
+                        LocaleListCompat.forLanguageTags(appLocales));
+            }
+            // setting ComponentEnabledSetting for app component using
+            // AppLocalesMetadataHolderService (used only for locales, thus minimizing
+            // the chances of conflicts). Setting it as ENABLED marks the success of app-locales
+            // sync from AndroidX to framework.
+            // Flag DONT_KILL_APP indicates that you don't want to kill the app containing the
+            // component.
+            context.getPackageManager().setComponentEnabledSetting(app_locales_component,
+                    COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ DONT_KILL_APP);
+        }
+    }
+
+    /**
+     * Implementation of {@link java.util.concurrent.Executor} that executes each runnable on a
+     * new thread.
+     */
+    static class ThreadPerTaskExecutor implements Executor {
+        @Override
+        public void execute(Runnable r) {
+            new Thread(r).start();
+        }
+    }
+
+    /**
+     * Implementation of {@link java.util.concurrent.Executor} that executes runnables serially
+     * by synchronizing the {@link Executor#execute(Runnable)} method and maintaining a tasks
+     * queue.
+     */
+    static class SerialExecutor implements Executor {
+        private final Object mLock = new Object();
+        final Queue<Runnable> mTasks = new ArrayDeque<>();
+        final Executor mExecutor;
+        Runnable mActive;
+
+        SerialExecutor(Executor executor) {
+            this.mExecutor = executor;
+        }
+
+        @Override
+        public void execute(final Runnable r) {
+            synchronized (mLock) {
+                mTasks.add(() -> {
+                    try {
+                        r.run();
+                    } finally {
+                        scheduleNext();
+                    }
+                });
+                if (mActive == null) {
+                    scheduleNext();
+                }
+            }
+        }
+
+        protected void scheduleNext() {
+            synchronized (mLock) {
+                if ((mActive = mTasks.poll()) != null) {
+                    mExecutor.execute(mActive);
+                }
+            }
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java
new file mode 100644
index 0000000..9684b92
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LocaleOverlayHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+package androidx.appcompat.app;
+
+import android.os.LocaleList;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Static utilities to overlay locales on top of another LocaleListCompat.
+ *
+ * <p>This is used to overlay application-specific locales on top of
+ * system locales.</p>
+ */
+@RequiresApi(24)
+final class LocaleOverlayHelper {
+
+    private LocaleOverlayHelper() {}
+
+    /**
+     * Combines the overlay locales and base locales.
+     *
+     * @return the combined {@link LocaleListCompat} if the overlay locales is not empty/null else
+     * returns an empty LocaleListCompat.
+     */
+    static LocaleListCompat combineLocalesIfOverlayExists(LocaleListCompat overlayLocales,
+            LocaleListCompat baseLocales) {
+        if (overlayLocales == null || overlayLocales.isEmpty()) {
+            return LocaleListCompat.getEmptyLocaleList();
+        }
+        return combineLocales(overlayLocales, baseLocales);
+    }
+
+    static LocaleListCompat combineLocalesIfOverlayExists(LocaleList overlayLocales,
+            LocaleList baseLocales) {
+        if (overlayLocales == null || overlayLocales.isEmpty()) {
+            return LocaleListCompat.getEmptyLocaleList();
+        }
+        return combineLocales(LocaleListCompat.wrap(overlayLocales),
+                LocaleListCompat.wrap(baseLocales));
+    }
+
+    /**
+     * Creates a combined {@link LocaleListCompat} by placing overlay locales before base
+     * locales and dropping duplicates from the base locales.
+     */
+    private static LocaleListCompat combineLocales(LocaleListCompat overlayLocales,
+            LocaleListCompat baseLocales) {
+        // using LinkedHashSet to drop duplicates.
+        Set<Locale> combinedLocales = new LinkedHashSet<>();
+        for (int i = 0; i < overlayLocales.size() + baseLocales.size(); i++) {
+            Locale currLocale;
+            if (i < overlayLocales.size()) {
+                currLocale = overlayLocales.get(i);
+            } else {
+                currLocale = baseLocales.get(i - overlayLocales.size());
+            }
+            if (currLocale != null) {
+                combinedLocales.add(currLocale);
+            }
+        }
+        return LocaleListCompat.create(combinedLocales.toArray(
+                new Locale[combinedLocales.size()]));
+    }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
index 89e3bb9..34a8e20 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActivityChooserModel.java
@@ -683,6 +683,7 @@
      *
      * @return Whether loading was performed.
      */
+    @SuppressWarnings("deprecation")
     private boolean loadActivitiesIfNeeded() {
         if (mReloadActivities && mIntent != null) {
             mReloadActivities = false;
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 6e9c5ba..6cc5e8b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -59,6 +59,7 @@
 import androidx.appcompat.content.res.AppCompatResources;
 import androidx.appcompat.view.ContextThemeWrapper;
 import androidx.appcompat.view.menu.ShowableListMenu;
+import androidx.core.util.ObjectsCompat;
 import androidx.core.view.TintableBackgroundView;
 import androidx.core.view.ViewCompat;
 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes;
@@ -1129,7 +1130,7 @@
                 @NonNull android.widget.ThemedSpinnerAdapter themedSpinnerAdapter,
                 @Nullable Resources.Theme theme
         ) {
-            if (themedSpinnerAdapter.getDropDownViewTheme() != theme) {
+            if (!ObjectsCompat.equals(themedSpinnerAdapter.getDropDownViewTheme(), theme)) {
                 themedSpinnerAdapter.setDropDownViewTheme(theme);
             }
         }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
index ff70a9c..561672f 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/DropDownListView.java
@@ -23,6 +23,7 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -33,10 +34,12 @@
 
 import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.R;
 import androidx.appcompat.graphics.drawable.DrawableWrapper;
 import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.os.BuildCompat;
 import androidx.core.view.ViewPropertyAnimatorCompat;
 import androidx.core.widget.ListViewAutoScrollHelper;
 
@@ -62,8 +65,6 @@
 
     private int mMotionPosition;
 
-    private Field mIsChildViewEnabled;
-
     private GateKeeperDrawable mSelector;
 
     /*
@@ -126,15 +127,25 @@
         super(context, null, R.attr.dropDownListViewStyle);
         mHijackFocus = hijackFocus;
         setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+    }
 
-        try {
-            mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
-            mIsChildViewEnabled.setAccessible(true);
-        } catch (NoSuchFieldException e) {
-            e.printStackTrace();
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    private boolean superIsSelectedChildViewEnabled() {
+        if (BuildCompat.isAtLeastT()) {
+            return Api33Impl.isSelectedChildViewEnabled(this);
+        } else {
+            return PreApi33Impl.isSelectedChildViewEnabled(this);
         }
     }
 
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    private void superSetSelectedChildViewEnabled(boolean enabled) {
+        if (BuildCompat.isAtLeastT()) {
+            Api33Impl.setSelectedChildViewEnabled(this, enabled);
+        } else {
+            PreApi33Impl.setSelectedChildViewEnabled(this, enabled);
+        }
+    }
 
     @Override
     public boolean isInTouchMode() {
@@ -624,18 +635,14 @@
         selectorRect.right += mSelectionRightPadding;
         selectorRect.bottom += mSelectionBottomPadding;
 
-        try {
-            // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
-            // modify its value
-            final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
-            if (sel.isEnabled() != isChildViewEnabled) {
-                mIsChildViewEnabled.set(this, !isChildViewEnabled);
-                if (position != INVALID_POSITION) {
-                    refreshDrawableState();
-                }
+        // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
+        // modify its value
+        final boolean isChildViewEnabled = superIsSelectedChildViewEnabled();
+        if (sel.isEnabled() != isChildViewEnabled) {
+            superSetSelectedChildViewEnabled(!isChildViewEnabled);
+            if (position != INVALID_POSITION) {
+                refreshDrawableState();
             }
-        } catch (IllegalAccessException e) {
-            e.printStackTrace();
         }
     }
 
@@ -803,4 +810,66 @@
             view.drawableHotspotChanged(x, y);
         }
     }
+
+    // TODO(b/221852137): Use @DeprecatedSinceApi(33).
+    @SuppressWarnings({"JavaReflectionMemberAccess", "CatchAndPrintStackTrace"})
+    static class PreApi33Impl {
+        private static final Field sIsChildViewEnabled;
+
+        static {
+            Field isChildViewEnabled = null;
+
+            try {
+                isChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
+                isChildViewEnabled.setAccessible(true);
+            } catch (NoSuchFieldException e) {
+                e.printStackTrace();
+            }
+
+            sIsChildViewEnabled = isChildViewEnabled;
+        }
+
+        private PreApi33Impl() {
+            // This class is not instantiable.
+        }
+
+        static boolean isSelectedChildViewEnabled(AbsListView view) {
+            if (sIsChildViewEnabled != null) {
+                try {
+                    return sIsChildViewEnabled.getBoolean(view);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+            }
+
+            return false;
+        }
+
+        static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
+            if (sIsChildViewEnabled != null) {
+                try {
+                    sIsChildViewEnabled.set(view, enabled);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static boolean isSelectedChildViewEnabled(AbsListView view) {
+            return view.isSelectedChildViewEnabled();
+        }
+
+        @DoNotInline
+        static void setSelectedChildViewEnabled(AbsListView view, boolean enabled) {
+            view.setSelectedChildViewEnabled(enabled);
+        }
+    }
 }
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
index 072c94a..3bd4492 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SearchView.java
@@ -900,6 +900,7 @@
         updateSubmitArea();
     }
 
+    @SuppressWarnings("deprecation")
     private boolean hasVoiceSearch() {
         if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
             Intent testIntent = null;
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
index 173e289..77e9dae 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/ThemedSpinnerAdapter.java
@@ -123,7 +123,7 @@
         public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
             if (theme == null) {
                 mDropDownInflater = null;
-            } else if (theme == mContext.getTheme()) {
+            } else if (theme.equals(mContext.getTheme())) {
                 mDropDownInflater = mInflater;
             } else {
                 final Context context = new ContextThemeWrapper(mContext, theme);
diff --git a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
index 7aba8a7..2628b2e 100644
--- a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
+++ b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
@@ -93,7 +93,7 @@
             PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(notes)
                     .build();
             return Futures.transformAsync(mAppSearchSessionFuture,
-                    session -> session.put(request), mExecutor);
+                    session -> session.putAsync(request), mExecutor);
         } catch (Exception e) {
             return Futures.immediateFailedFuture(e);
         }
@@ -112,7 +112,7 @@
     }
 
     private ListenableFuture<AppSearchSession> createLocalSession() {
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(mContext, DB_NAME)
                         .build()
         );
@@ -122,7 +122,7 @@
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder().setForceOverride(FORCE_OVERRIDE).build();
         return Futures.transformAsync(mAppSearchSessionFuture,
-                session -> session.setSchema(request),
+                session -> session.setSchemaAsync(request),
                 mExecutor);
     }
 
@@ -131,7 +131,7 @@
             SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
                     .build();
             return Futures.transformAsync(mAppSearchSessionFuture,
-                    session -> session.setSchema(request), mExecutor);
+                    session -> session.setSchemaAsync(request), mExecutor);
         } catch (AppSearchException e) {
             return Futures.immediateFailedFuture(e);
         }
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
index 720d679..fce8a0d 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
@@ -81,7 +81,7 @@
      *                            storage as the storage type for debugging.
      */
     @NonNull
-    public static ListenableFuture<DebugAppSearchManager> create(
+    public static ListenableFuture<DebugAppSearchManager> createAsync(
             @NonNull Context context,
             @NonNull ExecutorService executor, @NonNull String databaseName,
             @AppSearchDebugActivity.StorageType int storageType) throws AppSearchException {
@@ -98,14 +98,13 @@
             case AppSearchDebugActivity.STORAGE_TYPE_LOCAL:
                 debugAppSearchManagerListenableFuture =
                         Futures.transform(
-                                debugAppSearchManager.initializeLocalStorage(databaseName),
+                                debugAppSearchManager.initializeLocalStorageAsync(databaseName),
                                 unused -> debugAppSearchManager, executor);
                 break;
             case AppSearchDebugActivity.STORAGE_TYPE_PLATFORM:
                 if (Build.VERSION.SDK_INT >= 31) {
-                    debugAppSearchManagerListenableFuture =
-                            Futures.transform(
-                                    debugAppSearchManager.initializePlatformStorage(databaseName),
+                    debugAppSearchManagerListenableFuture = Futures.transform(
+                            debugAppSearchManager.initializePlatformStorageAsync(databaseName),
                                     unused -> debugAppSearchManager, executor);
                 } else {
                     throw new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
@@ -130,7 +129,7 @@
      * {@link #getNextPage} to retrieve the {@link GenericDocument} objects for each page.
      */
     @NonNull
-    public ListenableFuture<SearchResults> getAllDocumentsSearchResults() {
+    public ListenableFuture<SearchResults> getAllDocumentsSearchResultsAsync() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setResultCountPerPage(PAGE_SIZE)
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -151,10 +150,11 @@
      *                {@link GenericDocument} objects.
      */
     @NonNull
-    public ListenableFuture<List<GenericDocument>> getNextPage(@NonNull SearchResults results) {
+    public ListenableFuture<List<GenericDocument>> getNextPageAsync(
+            @NonNull SearchResults results) {
         Preconditions.checkNotNull(results);
 
-        return Futures.transform(results.getNextPage(),
+        return Futures.transform(results.getNextPageAsync(),
                 DebugAppSearchManager::convertResultsToGenericDocuments, mExecutor);
     }
 
@@ -162,7 +162,7 @@
      * Gets a document from the AppSearch database by namespace and ID.
      */
     @NonNull
-    public ListenableFuture<GenericDocument> getDocument(@NonNull String namespace,
+    public ListenableFuture<GenericDocument> getDocumentAsync(@NonNull String namespace,
             @NonNull String id) {
         Preconditions.checkNotNull(id);
         Preconditions.checkNotNull(namespace);
@@ -170,7 +170,7 @@
                 new GetByDocumentIdRequest.Builder(namespace).addIds(id).build();
 
         return Futures.transformAsync(mAppSearchSessionFuture,
-                session -> Futures.transform(session.getByDocumentId(request),
+                session -> Futures.transform(session.getByDocumentIdAsync(request),
                         response -> response.getSuccesses().get(id), mExecutor), mExecutor);
     }
 
@@ -178,9 +178,9 @@
      * Gets the schema of the AppSearch database.
      */
     @NonNull
-    public ListenableFuture<GetSchemaResponse> getSchema() {
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
         return Futures.transformAsync(mAppSearchSessionFuture,
-                session -> session.getSchema(), mExecutor);
+                session -> session.getSchemaAsync(), mExecutor);
     }
 
     /**
@@ -195,9 +195,9 @@
     }
 
     @NonNull
-    private ListenableFuture<AppSearchSession> initializeLocalStorage(
+    private ListenableFuture<AppSearchSession> initializeLocalStorageAsync(
             @NonNull String databaseName) {
-        mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
+        mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(mContext, databaseName)
                         .build())
         );
@@ -206,9 +206,9 @@
 
     @NonNull
     @RequiresApi(Build.VERSION_CODES.S)
-    private ListenableFuture<AppSearchSession> initializePlatformStorage(
+    private ListenableFuture<AppSearchSession> initializePlatformStorageAsync(
             @NonNull String databaseName) {
-        mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSession(
+        mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSessionAsync(
                 new PlatformStorage.SearchContext.Builder(mContext, databaseName)
                         .build())
         );
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
index 5a23cb0..6425c63 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
@@ -76,7 +76,7 @@
      */
     @NonNull
     public LiveData<SearchResults> getAllDocumentsSearchResults() {
-        Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResults(),
+        Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResultsAsync(),
                 new FutureCallback<SearchResults>() {
                     @Override
                     public void onSuccess(SearchResults result) {
@@ -107,7 +107,7 @@
     @NonNull
     public LiveData<List<GenericDocument>> addAdditionalResultsPage(
             @NonNull SearchResults results) {
-        Futures.addCallback(mDebugAppSearchManager.getNextPage(results),
+        Futures.addCallback(mDebugAppSearchManager.getNextPageAsync(results),
                 new FutureCallback<List<GenericDocument>>() {
                     @Override
                     public void onSuccess(List<GenericDocument> result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
index a8c03dc..41d2ed1 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentModel.java
@@ -61,7 +61,7 @@
      */
     @NonNull
     public LiveData<GenericDocument> getDocument(@NonNull String namespace, @NonNull String id) {
-        Futures.addCallback(mDebugAppSearchManager.getDocument(namespace, id),
+        Futures.addCallback(mDebugAppSearchManager.getDocumentAsync(namespace, id),
                 new FutureCallback<GenericDocument>() {
                     @Override
                     public void onSuccess(GenericDocument result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
index 16e0b66..70cc1ee 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/model/SchemaTypeListModel.java
@@ -90,7 +90,7 @@
      */
     @NonNull
     private LiveData<GetSchemaResponse> getSchema() {
-        Futures.addCallback(mDebugAppSearchManager.getSchema(),
+        Futures.addCallback(mDebugAppSearchManager.getSchemaAsync(),
                 new FutureCallback<GetSchemaResponse>() {
                     @Override
                     public void onSuccess(GetSchemaResponse result) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
index 20ea01f..3174e2d 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
@@ -97,7 +97,7 @@
         @StorageType int storageType =
                 getIntent().getExtras().getInt(STORAGE_TYPE_INTENT_KEY);
         try {
-            mDebugAppSearchManager = DebugAppSearchManager.create(
+            mDebugAppSearchManager = DebugAppSearchManager.createAsync(
                     getApplicationContext(), mBackgroundExecutor, mDbName, storageType);
         } catch (AppSearchException e) {
             Toast.makeText(getApplicationContext(),
diff --git a/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
index 7636104..85eb735 100644
--- a/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
+++ b/appsearch/appsearch-ktx/src/androidTest/java/AnnotationProcessorKtTest.kt
@@ -43,7 +43,7 @@
     @Before
     public fun setUp() {
         val context = ApplicationProvider.getApplicationContext<Context>()
-        session = LocalStorage.createSearchSession(
+        session = LocalStorage.createSearchSessionAsync(
             LocalStorage.SearchContext.Builder(context, DB_NAME).build()
         ).get()
 
@@ -59,7 +59,7 @@
     }
 
     private fun cleanup() {
-        session.setSchema(SetSchemaRequest.Builder().setForceOverride(true).build()).get()
+        session.setSchemaAsync(SetSchemaRequest.Builder().setForceOverride(true).build()).get()
     }
 
     @Document
@@ -250,7 +250,7 @@
 
     @Test
     fun testAnnotationProcessor() {
-        session.setSchema(
+        session.setSchemaAsync(
             SetSchemaRequest.Builder()
                 .addDocumentClasses(Card::class.java, Gift::class.java).build()
         ).get()
@@ -260,7 +260,7 @@
 
         // Index the Gift document and query it.
         checkIsBatchResultSuccess(
-            session.put(
+            session.putAsync(
                 PutDocumentsRequest.Builder().addDocuments(inputDocument).build()
             )
         )
diff --git a/appsearch/appsearch-local-storage/api/current.txt b/appsearch/appsearch-local-storage/api/current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/current.txt
+++ b/appsearch/appsearch-local-storage/api/current.txt
@@ -2,7 +2,8 @@
 package androidx.appsearch.localstorage {
 
   public class LocalStorage {
-    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
   public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-local-storage/api/public_plus_experimental_current.txt
@@ -2,7 +2,8 @@
 package androidx.appsearch.localstorage {
 
   public class LocalStorage {
-    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
   public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/api/restricted_current.txt b/appsearch/appsearch-local-storage/api/restricted_current.txt
index 5eb0af5..94465dd 100644
--- a/appsearch/appsearch-local-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-local-storage/api/restricted_current.txt
@@ -2,7 +2,8 @@
 package androidx.appsearch.localstorage {
 
   public class LocalStorage {
-    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
   public static final class LocalStorage.SearchContext {
diff --git a/appsearch/appsearch-local-storage/build.gradle b/appsearch/appsearch-local-storage/build.gradle
index 268af48..d723e00 100644
--- a/appsearch/appsearch-local-storage/build.gradle
+++ b/appsearch/appsearch-local-storage/build.gradle
@@ -44,12 +44,11 @@
     defaultConfig {
         externalNativeBuild {
             cmake {
-                cppFlags "-std=c++17"
                 arguments "-DCMAKE_VERBOSE_MAKEFILE=ON"
                 targets "icing"
             }
         }
-	multiDexEnabled true
+        multiDexEnabled true
     }
     externalNativeBuild {
         cmake {
@@ -72,9 +71,9 @@
     api("androidx.annotation:annotation:1.1.0")
 
     implementation(project(":appsearch:appsearch"))
+    implementation('androidx.collection:collection:1.2.0')
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation("androidx.core:core:1.2.0")
-    implementation('androidx.collection:collection:1.0.0')
 
     androidTestImplementation project(':appsearch:appsearch-test-util')
     androidTestImplementation(libs.multidex)
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 961543b..22128b0 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -25,20 +25,28 @@
 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.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
 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.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.stats.InitializeStats;
 import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.appsearch.testutil.TestObserverCallback;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.test.core.app.ApplicationProvider;
@@ -58,6 +66,7 @@
 import com.google.android.icing.proto.TermMatchType;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.After;
 import org.junit.Before;
@@ -72,6 +81,7 @@
 import java.util.Map;
 import java.util.Set;
 
+@SuppressWarnings("GuardedBy")
 public class AppSearchImplTest {
     /**
      * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
@@ -80,6 +90,12 @@
     @Rule
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
     private File mAppSearchDir;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    // The caller access for this package
+    private final CallerAccess mSelfCallerAccess = new CallerAccess(mContext.getPackageName());
+
     private AppSearchImpl mAppSearchImpl;
 
     @Before
@@ -88,7 +104,9 @@
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
                 new UnlimitedLimitConfig(),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
     }
 
     @After
@@ -366,9 +384,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -409,17 +425,14 @@
     @Test
     public void testReset() throws Exception {
         // Insert schema
-        Context context = ApplicationProvider.getApplicationContext();
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
                 new AppSearchSchema.Builder("Type2").build());
         mAppSearchImpl.setSchema(
-                context.getPackageName(),
+                mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -428,7 +441,7 @@
         GenericDocument validDoc =
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
         mAppSearchImpl.putDocument(
-                context.getPackageName(),
+                mContext.getPackageName(),
                 "database1",
                 validDoc,
                 /*logger=*/null);
@@ -438,10 +451,7 @@
         SearchResultPage results = mAppSearchImpl.globalQuery(
                 /*queryExpression=*/ "",
                 new SearchSpec.Builder().addFilterSchemas("Type1").build(),
-                context.getPackageName(),
-                /*visibilityStore=*/ null,
-                Process.INVALID_UID,
-                /*callerHasSystemAccess=*/ false,
+                mSelfCallerAccess,
                 /*logger=*/ null);
         assertThat(results.getResults()).hasSize(1);
         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -450,7 +460,7 @@
         DocumentProto invalidDoc = DocumentProto.newBuilder()
                 .setNamespace("invalidNamespace")
                 .setUri("id2")
-                .setSchema(context.getPackageName() + "$database1/Type1")
+                .setSchema(mContext.getPackageName() + "$database1/Type1")
                 .build();
         AppSearchException e = assertThrows(
                 AppSearchException.class,
@@ -466,7 +476,8 @@
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
-                mAppSearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+                mAppSearchDir, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Check recovery state
         InitializeStats initStats = initStatsBuilder.build();
@@ -485,33 +496,32 @@
         assertThat(initStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
 
         // Make sure all our data is gone
-        assertThat(mAppSearchImpl.getSchema(context.getPackageName(), "database1").getSchemas())
+        assertThat(mAppSearchImpl.getSchema(
+                /*packageName=*/mContext.getPackageName(),
+                /*databaseName=*/"database1",
+                /*callerAccess=*/mSelfCallerAccess)
+                .getSchemas())
                 .isEmpty();
         results = mAppSearchImpl.globalQuery(
                 /*queryExpression=*/ "",
                 new SearchSpec.Builder().addFilterSchemas("Type1").build(),
-                context.getPackageName(),
-                /*visibilityStore=*/ null,
-                Process.INVALID_UID,
-                /*callerHasSystemAccess=*/ false,
+                mSelfCallerAccess,
                 /*logger=*/ null);
         assertThat(results.getResults()).isEmpty();
 
         // Make sure the index can now be used successfully
         mAppSearchImpl.setSchema(
-                context.getPackageName(),
+                mContext.getPackageName(),
                 "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // Insert a valid doc
         mAppSearchImpl.putDocument(
-                context.getPackageName(),
+                mContext.getPackageName(),
                 "database1",
                 validDoc,
                 /*logger=*/null);
@@ -520,10 +530,7 @@
         results = mAppSearchImpl.globalQuery(
                 /*queryExpression=*/ "",
                 new SearchSpec.Builder().addFilterSchemas("Type1").build(),
-                context.getPackageName(),
-                /*visibilityStore=*/ null,
-                Process.INVALID_UID,
-                /*callerHasSystemAccess=*/ false,
+                mSelfCallerAccess,
                 /*logger=*/ null);
         assertThat(results.getResults()).hasSize(1);
         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -551,9 +558,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -565,9 +570,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -608,9 +611,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -622,9 +623,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -660,16 +659,13 @@
     }
 
     @Test
-    public void testGlobalQueryEmptyDatabase() throws Exception {
+    public void testGlobalQuery_emptyPackage() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
-                "",
+                /*queryExpression=*/"",
                 searchSpec,
-                /*callerPackageName=*/ "",
-                /*visibilityStore=*/ null,
-                Process.INVALID_UID,
-                /*callerHasSystemAccess=*/ false,
+                new CallerAccess(/*callingPackageName=*/""),
                 /*logger=*/ null);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
@@ -683,9 +679,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -727,9 +721,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -781,9 +773,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -801,9 +791,11 @@
                 .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                 .setResultCountPerPage(1)
                 .build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
-                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
-                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                searchSpec,
+                new CallerAccess(/*callingPackageName=*/"package1"),
+                /*logger=*/ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -826,9 +818,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -846,9 +836,11 @@
                 .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                 .setResultCountPerPage(1)
                 .build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
-                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
-                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                searchSpec,
+                new CallerAccess(/*callingPackageName=*/"package1"),
+                /*logger=*/ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -881,9 +873,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -932,9 +922,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -972,9 +960,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1026,9 +1012,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1046,9 +1030,11 @@
                 .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                 .setResultCountPerPage(1)
                 .build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
-                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
-                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                searchSpec,
+                new CallerAccess(/*callingPackageName=*/"package1"),
+                /*logger=*/ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1078,9 +1064,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1098,9 +1082,11 @@
                 .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                 .setResultCountPerPage(1)
                 .build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(/*queryExpression=*/ "",
-                searchSpec, "package1", /*visibilityStore=*/ null, Process.myUid(),
-                /*callerHasSystemAccess=*/ false, /*logger=*/ null);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery(
+                /*queryExpression=*/ "",
+                searchSpec,
+                new CallerAccess(/*callingPackageName=*/"package1"),
+                /*logger=*/ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1155,9 +1141,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1193,9 +1177,7 @@
                 "package",
                 "database1",
                 oldSchemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1209,9 +1191,7 @@
                 "package",
                 "database1",
                 newSchemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1232,9 +1212,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1262,9 +1240,7 @@
                         "package",
                         "database1",
                         finalSchemas,
-                        /*visibilityStore=*/ null,
-                        /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                        /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                        /*visibilityDocuments=*/ Collections.emptyList(),
                         /*forceOverride=*/ false,
                         /*version=*/ 0,
                         /* setSchemaStatsBuilder= */ null);
@@ -1276,9 +1252,7 @@
                 "package",
                 "database1",
                 finalSchemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1312,9 +1286,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1322,9 +1294,7 @@
                 "package",
                 "database2",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1356,9 +1326,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1397,9 +1365,7 @@
                 "package",
                 "database",
                 schema,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1444,6 +1410,13 @@
             existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
         }
 
+        // Create VisibilityDocument
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("schema")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+
         // Insert schema for package A and B.
         List<AppSearchSchema> schema =
                 ImmutableList.of(new AppSearchSchema.Builder("schema").build());
@@ -1451,9 +1424,7 @@
                 "packageA",
                 "database",
                 schema,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1461,14 +1432,12 @@
                 "packageB",
                 "database",
                 schema,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
 
-        // Verify these two packages is stored in AppSearch
+        // Verify these two packages are stored in AppSearch.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
@@ -1483,6 +1452,26 @@
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
+        // Verify these two visibility documents are stored in AppSearch.
+        VisibilityDocument expectedVisibilityDocumentA =
+                new VisibilityDocument.Builder("packageA$database/schema")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .setCreationTimestampMillis(12345L)
+                        .build();
+        VisibilityDocument expectedVisibilityDocumentB =
+                new VisibilityDocument.Builder("packageB$database/schema")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .setCreationTimestampMillis(12345L)
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility("packageA$database/schema"))
+                .isEqualTo(expectedVisibilityDocumentA);
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility("packageB$database/schema"))
+                .isEqualTo(expectedVisibilityDocumentB);
+
         // Prune packages
         mAppSearchImpl.prunePackageData(existingPackages);
 
@@ -1491,6 +1480,12 @@
                 .containsExactlyElementsIn(existingSchemas);
         assertThat(mAppSearchImpl.getPackageToDatabases())
                 .containsExactlyEntriesIn(existingDatabases);
+
+        // Verify the VisibilitySetting is removed.
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility("packageA$database/schema")).isNull();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility("packageB$database/schema")).isNull();
     }
 
     @Test
@@ -1504,9 +1499,7 @@
         mAppSearchImpl.setSchema(
                 "package1", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1518,9 +1511,7 @@
         mAppSearchImpl.setSchema(
                 "package1", "database2",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1532,9 +1523,7 @@
         mAppSearchImpl.setSchema(
                 "package2", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1542,6 +1531,47 @@
                 expectedMapping);
     }
 
+    @Test
+    public void testGetAllPrefixedSchemaTypes() throws Exception {
+        // Insert schema
+        List<AppSearchSchema> schemas1 =
+                Collections.singletonList(new AppSearchSchema.Builder("type1").build());
+        List<AppSearchSchema> schemas2 =
+                Collections.singletonList(new AppSearchSchema.Builder("type2").build());
+        List<AppSearchSchema> schemas3 =
+                Collections.singletonList(new AppSearchSchema.Builder("type3").build());
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database1",
+                schemas1,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database2",
+                schemas2,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database1",
+                schemas3,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        assertThat(mAppSearchImpl.getAllPrefixedSchemaTypes()).containsExactly(
+                "package1$database1/type1",
+                "package1$database2/type2",
+                "package2$database1/type3",
+                "VS#Pkg$VS#Db/VisibilityType",  // plus the stored Visibility schema
+                "VS#Pkg$VS#Db/VisibilityPermissionType");
+    }
+
     @FlakyTest(bugId = 204186664)
     @Test
     public void testReportUsage() throws Exception {
@@ -1552,9 +1582,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1642,9 +1670,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1666,9 +1692,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1683,9 +1707,7 @@
                 "package2",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1732,9 +1754,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1756,9 +1776,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1779,9 +1797,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1789,9 +1805,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1834,9 +1848,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1848,15 +1860,15 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null));
 
         assertThrows(IllegalStateException.class, () -> mAppSearchImpl.getSchema(
-                "package", "database"));
+                /*packageName=*/"package",
+                /*databaseName=*/"database",
+                /*callerAccess=*/mSelfCallerAccess));
 
         assertThrows(IllegalStateException.class, () -> mAppSearchImpl.putDocument(
                 "package",
@@ -1877,10 +1889,7 @@
         assertThrows(IllegalStateException.class, () -> mAppSearchImpl.globalQuery(
                 "query",
                 new SearchSpec.Builder().build(),
-                "package",
-                /*visibilityStore=*/ null,
-                Process.INVALID_UID,
-                /*callerHasSystemAccess=*/ false,
+                mSelfCallerAccess,
                 /*logger=*/ null));
 
         assertThrows(IllegalStateException.class, () -> mAppSearchImpl.getNextPage("package",
@@ -1922,9 +1931,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1945,7 +1952,8 @@
                 mAppSearchDir,
                 new UnlimitedLimitConfig(),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
                 "id1",
                 Collections.emptyMap());
@@ -1961,9 +1969,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2004,7 +2010,8 @@
                 mAppSearchDir,
                 new UnlimitedLimitConfig(),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -2025,9 +2032,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2070,7 +2075,8 @@
                 mAppSearchDir,
                 new UnlimitedLimitConfig(),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -2091,9 +2097,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2116,7 +2120,7 @@
                 .isEqualTo(2);
         assertThat(
                 storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
-                .isEqualTo(1);
+                .isEqualTo(3); // +2 for VisibilitySchema
     }
 
     @Test
@@ -2136,7 +2140,8 @@
                         return 1;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -2145,9 +2150,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2196,7 +2199,8 @@
                         return 1;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -2205,9 +2209,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2243,7 +2245,8 @@
                         return 1;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Make sure the limit is maintained
         e = assertThrows(AppSearchException.class, () ->
@@ -2270,7 +2273,8 @@
                         return 3;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -2279,9 +2283,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2360,7 +2362,8 @@
                         return 2;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -2369,9 +2372,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2379,9 +2380,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2389,9 +2388,7 @@
                 "package2",
                 "database1",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2399,9 +2396,7 @@
                 "package2",
                 "database2",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2451,7 +2446,8 @@
                         return 2;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // package1 should still be out of space
         e = assertThrows(AppSearchException.class, () ->
@@ -2500,7 +2496,8 @@
                         return 3;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2516,9 +2513,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2614,7 +2609,8 @@
                         return 2;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2626,9 +2622,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2685,7 +2679,8 @@
                         return 2;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -2697,9 +2692,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2736,7 +2729,8 @@
                         return 2;
                     }
                 },
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         // Index id2. This should pass but only because we check for replacements.
         mAppSearchImpl.putDocument(
@@ -2754,4 +2748,1511 @@
         assertThat(e).hasMessageThat().contains(
                 "Package \"package\" exceeded limit of 2 documents");
     }
+
+    /**
+     * Ensure that it is okay to register the same observer for multiple packages and that removing
+     * the observer for one package doesn't remove it for the other.
+     */
+    @Test
+    public void testRemoveObserver_onlyAffectsOnePackage() throws Exception {
+        final String fakePackage = "com.android.appsearch.fake.package";
+
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                /*schemas=*/ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/false,
+                /*version=*/0,
+                /*setSchemaStatsBuilder=*/null);
+
+        // Register an observer twice, on different packages.
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/fakePackage,
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a valid doc
+        GenericDocument validDoc =
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                validDoc,
+                /*logger=*/null);
+
+        // Dispatch notifications and empty the observers
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        observer.clear();
+
+        // Remove the observer from the fake package
+        mAppSearchImpl.unregisterObserverCallback(fakePackage, observer);
+
+        // Index a second document
+        GenericDocument doc2 = new GenericDocument.Builder<>("namespace1", "id2", "Type1").build();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                doc2,
+                /*logger=*/null);
+
+        // Observer should still have received this data from its registration on
+        // context.getPackageName(), as we only removed the copy from fakePackage.
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).containsExactly(
+                new DocumentChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        "namespace1",
+                        "Type1",
+                        /*changedDocumentIds=*/ImmutableSet.of("id2")));
+    }
+
+    @Test
+    public void testGetGlobalDocumentThrowsExceptionWhenNotVisible() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.globalGetDocument(
+                        "package",
+                        "database",
+                        "namespace1",
+                        "id1",
+                        /*typePropertyPaths=*/Collections.emptyMap(),
+                        /*callerAccess=*/mSelfCallerAccess));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(e.getMessage()).isEqualTo("Document (namespace1, id1) not found.");
+    }
+
+    @Test
+    public void testGetGlobalDocument() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = mAppSearchImpl.globalGetDocument(
+                "package",
+                "database",
+                "namespace1",
+                "id1",
+                /*typePropertyPaths=*/Collections.emptyMap(),
+                /*callerAccess=*/mSelfCallerAccess);
+        assertThat(getResult).isEqualTo(document);
+    }
+
+    @Test
+    public void getGlobalDocumentTest_notFound() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        AppSearchException e = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.globalGetDocument(
+                        "package",
+                        "database",
+                        "namespace1",
+                        "id2",
+                        /*typePropertyPaths=*/Collections.emptyMap(),
+                        /*callerAccess=*/mSelfCallerAccess));
+        assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+        assertThat(e.getMessage()).isEqualTo("Document (namespace1, id2) not found.");
+    }
+
+    @Test
+    public void getGlobalDocumentNoAccessNoFileHasSameException() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) ->
+                        callerAccess.getCallingPackageName().equals("visiblePackage");
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        AppSearchException unauthorizedException = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.globalGetDocument(
+                        "package",
+                        "database",
+                        "namespace1",
+                        "id1",
+                        /*typePropertyPaths=*/Collections.emptyMap(),
+                        new CallerAccess(/*callingPackageName=*/"invisiblePackage")));
+
+        mAppSearchImpl.remove("package", "database", "namespace1", "id1",
+                /*removeStatsBuilder=*/null);
+
+        AppSearchException noDocException = assertThrows(AppSearchException.class, () ->
+                mAppSearchImpl.globalGetDocument(
+                        "package",
+                        "database",
+                        "namespace1",
+                        "id1",
+                        /*typePropertyPaths=*/Collections.emptyMap(),
+                        new CallerAccess(/*callingPackageName=*/"visiblePackage")));
+
+        assertThat(noDocException.getResultCode()).isEqualTo(unauthorizedException.getResultCode());
+        assertThat(noDocException.getMessage()).isEqualTo(unauthorizedException.getMessage());
+    }
+
+    @Test
+    public void testSetVisibility() throws Exception {
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+
+        // Set schema Email to AppSearch database1 with a visibility document
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        String prefix = PrefixUtil.createPrefix("package", "database1");
+
+        // assert the visibility document is saved.
+        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+                .isEqualTo(expectedDocument);
+    }
+
+    @Test
+    public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
+        // Create Visibility Document for Email1
+        VisibilityDocument visibilityDocument1 = new VisibilityDocument.Builder("Email1")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        List<AppSearchSchema> schemas1 =
+                Collections.singletonList(new AppSearchSchema.Builder("Email1").build());
+
+        // Set schema Email1 to package1 with a visibility document
+        mAppSearchImpl.setSchema(
+                "package1",
+                "database",
+                schemas1,
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument1),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        String prefix1 = PrefixUtil.createPrefix("package1", "database");
+
+        // assert the visibility document is saved.
+        VisibilityDocument expectedDocument1 = new VisibilityDocument.Builder(prefix1 + "Email1")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+                .isEqualTo(expectedDocument1);
+
+        // Create Visibility Document for Email2
+        VisibilityDocument visibilityDocument2 = new VisibilityDocument.Builder("Email2")
+                .setNotDisplayedBySystem(false)
+                .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                .setCreationTimestampMillis(54321L)
+                .build();
+        List<AppSearchSchema> schemas2 =
+                Collections.singletonList(new AppSearchSchema.Builder("Email2").build());
+
+        // Set schema Email2 to package1 with a visibility document
+        mAppSearchImpl.setSchema(
+                "package2",
+                "database",
+                schemas2,
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument2),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+        // assert the visibility document is saved.
+        VisibilityDocument expectedDocument2 = new VisibilityDocument.Builder(prefix2 + "Email2")
+                .setNotDisplayedBySystem(false)
+                .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                .setCreationTimestampMillis(54321)
+                .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
+                .isEqualTo(expectedDocument2);
+
+        // Check the existing visibility document retains.
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+                .isEqualTo(expectedDocument1);
+    }
+
+    @Test
+    public void testSetVisibility_removeVisibilitySettings() throws Exception {
+        // Create a non-all-default visibility document
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+
+        // Set schema Email and its visibility document to AppSearch database1
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        String prefix = PrefixUtil.createPrefix("package", "database1");
+        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+                .isEqualTo(expectedDocument);
+
+        // Set schema Email and its all-default visibility document to AppSearch database1
+        mAppSearchImpl.setSchema(
+                "package",
+                "database1",
+                schemas,
+                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /* setSchemaStatsBuilder= */ null);
+        // All-default visibility document won't be saved in AppSearch.
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+                .isNull();
+    }
+
+    @Test
+    public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
+        // set Schema and visibility to AppSearch
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
+        mAppSearchImpl.setSchema(
+                "packageName",
+                "databaseName",
+                schemas,
+                ImmutableList.of(visibilityDocument),
+                /*forceOverride=*/ true,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // close and re-open AppSearchImpl, the visibility document retains
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .setCreationTimestampMillis(12345L)
+                .build();
+
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+                .isEqualTo(expectedDocument);
+
+        // remove schema and visibility document
+        mAppSearchImpl.setSchema(
+                "packageName",
+                "databaseName",
+                ImmutableList.of(),
+                ImmutableList.of(),
+                /*forceOverride=*/ true,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // close and re-open AppSearchImpl, the visibility document removed
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
+    }
+
+    @Test
+    public void testGetSchema_global() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Type").build());
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ImmutableList.of(
+                        new VisibilityDocument.Builder("Type")
+                                .setNotDisplayedBySystem(true).build()),
+                /*forceOverride=*/false,
+                /*version=*/0,
+                /*setSchemaStatsBuilder=*/null);
+
+        // Get this schema as another package
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(/*callingPackageName=*/"com.android.appsearch.fake.package"));
+        assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+        assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("Type");
+    }
+
+    @Test
+    public void testGetSchema_nonExistentApp() throws Exception {
+        // Add a schema. The test loses meaning if the schema is completely empty.
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
+                /*visibilityDocuments=*/ImmutableList.of(),
+                /*forceOverride=*/false,
+                /*version=*/0,
+                /*setSchemaStatsBuilder=*/null);
+
+        // Try to get the schema of a nonexistent package.
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "com.android.appsearch.fake.package",
+                "database",
+                new CallerAccess(/*callingPackageName=*/"package"));
+        assertThat(getResponse.getSchemas()).isEmpty();
+        assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+    }
+
+    @Test
+    public void testGetSchema_noAccess() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Type").build());
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ImmutableList.of(),
+                /*forceOverride=*/false,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(/*callingPackageName=*/
+                        "com.android.appsearch.fake.package"));
+        assertThat(getResponse.getSchemas()).isEmpty();
+        assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+        assertThat(getResponse.getVersion()).isEqualTo(0);
+
+        // Make sure the test is hooked up right by calling getSchema with the same parameters but
+        // from the same package
+        getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(/*callingPackageName=*/"package"));
+        assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+    }
+
+    @Test
+    public void testGetSchema_global_partialAccess() throws Exception {
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("VisibleType").build(),
+                new AppSearchSchema.Builder("PrivateType").build());
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+        VisibilityChecker mockVisibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> prefixedSchema.endsWith("VisibleType");
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                mockVisibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                /*visibilityDocuments=*/ImmutableList.of(
+                        new VisibilityDocument.Builder("VisibleType")
+                                .setNotDisplayedBySystem(true)
+                                .build(),
+                        new VisibilityDocument.Builder("PrivateType")
+                                .setNotDisplayedBySystem(true)
+                                .build()),
+                /*forceOverride=*/false,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(/*callingPackageName=*/
+                        "com.android.appsearch.fake.package"));
+        assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
+        assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("VisibleType");
+        assertThat(getResponse.getVersion()).isEqualTo(1);
+    }
+
+    @Test
+    public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a valid doc
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+                /*logger=*/null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).containsExactly(
+                new DocumentChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        "namespace1",
+                        "Type1",
+                        ImmutableSet.of("id1")));
+    }
+
+    @Test
+    public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
+        // Make a visibility checker that rejects everything
+        final VisibilityChecker rejectChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                rejectChecker);
+
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a valid doc
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+                /*logger=*/null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).containsExactly(
+                new DocumentChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        "namespace1",
+                        "Type1",
+                        ImmutableSet.of("id1")));
+    }
+
+    @Test
+    public void testDispatchObserver_differentPackage_noVisStore_reject() throws Exception {
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer from a simulated different package
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/
+                    "com.fake.Listening.package"),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a valid doc
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+                /*logger=*/null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testDispatchObserver_differentPackage_withVisStore_accept() throws Exception {
+        final String fakeListeningPackage = "com.fake.listening.package";
+
+        // Make a visibility checker that allows only fakeListeningPackage.
+        final VisibilityChecker visibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                visibilityChecker);
+
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a valid doc
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+                /*logger=*/null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).containsExactly(
+                new DocumentChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        "namespace1",
+                        "Type1",
+                        ImmutableSet.of("id1")));
+    }
+
+    @Test
+    public void testDispatchObserver_differentPackage_withVisStore_reject() throws Exception {
+        final String fakeListeningPackage = "com.fake.Listening.package";
+
+        // Make a visibility checker that rejects everything.
+        final VisibilityChecker rejectChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                rejectChecker);
+
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Insert a doc
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.putDocument(
+                mContext.getPackageName(),
+                "database1",
+                new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
+                /*logger=*/null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_added() throws Exception {
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Add a schema type
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        ImmutableSet.of("Type1")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Add two more schema types without touching the existing one
+        observer.clear();
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2").build(),
+                        new AppSearchSchema.Builder("Type3").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Dispatch notifications
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2", "Type3")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_removed() throws Exception {
+        // Add a schema type
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Remove Type2
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(),
+                        "database1",
+                        ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_contents() throws Exception {
+        // Add a schema
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Update the schema, but don't make any actual changes
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 1,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Now update the schema again, but this time actually make a change (cardinality of the
+        // property)
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 2,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+        // Add a schema
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer that only listens for Type2
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Update both types of the schema (changed cardinalities)
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_visibilityOnly() throws Exception {
+        final String fakeListeningPackage = "com.fake.listening.package";
+
+        // Make a fake visibility checker that actually looks at visibility store
+        final VisibilityChecker visibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> {
+                    if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
+                        return false;
+                    }
+                    Set<String> allowedPackages = new ArraySet<>(
+                            visibilityStore.getVisibility(prefixedSchema).getPackageNames());
+                    return allowedPackages.contains(fakeListeningPackage);
+                };
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                visibilityChecker);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Add a schema where both types are visible to the fake package.
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("Type1").build(),
+                new AppSearchSchema.Builder("Type2").build());
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                schemas,
+                /*visibilityDocuments=*/ ImmutableList.of(
+                        new VisibilityDocument.Builder("Type1")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build(),
+                        new VisibilityDocument.Builder("Type2")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build()
+                ),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Notifications of addition should now be dispatched
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type1", "Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        observer.clear();
+
+        // Update schema, keeping the types identical but denying visibility to type2
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                schemas,
+                /*visibilityDocuments=*/ ImmutableList.of(
+                        new VisibilityDocument.Builder("Type1")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build(),
+                        new VisibilityDocument.Builder("Type2").build()
+                ),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications. This should look like a deletion of Type2.
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        observer.clear();
+
+        // Now update Type2 and make sure no further notification is received.
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ ImmutableList.of(
+                        new VisibilityDocument.Builder("Type1")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build(),
+                        new VisibilityDocument.Builder("Type2").build()
+                ),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Grant visibility to Type2 again and make sure it appears
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ImmutableList.of(
+                        new VisibilityDocument.Builder("Type1")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build(),
+                        new VisibilityDocument.Builder("Type2")
+                                .addVisibleToPackage(
+                                        new PackageIdentifier(fakeListeningPackage, new byte[0]))
+                                .build()
+                ),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications. This should look like a creation of Type2.
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_visibilityAndContents() throws Exception {
+        final String fakeListeningPackage = "com.fake.listening.package";
+
+        // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+        final VisibilityChecker visibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+                        && prefixedSchema.endsWith("Type2");
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                visibilityChecker);
+
+        // Add a schema.
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Update both types of the schema (changed cardinalities)
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build(),
+                        new AppSearchSchema.Builder("Type2")
+                                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        "booleanProp")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                                .build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_partialVisibility_removed() throws Exception {
+        final String fakeListeningPackage = "com.fake.listening.package";
+
+        // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+        final VisibilityChecker visibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+                        && prefixedSchema.endsWith("Type2");
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                visibilityChecker);
+
+        // Add a schema.
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakeListeningPackage),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observer);
+
+        // Remove Type1
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications. Nothing should appear since Type1 is not visible to us.
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Now remove Type2. This should cause a notification.
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_multipleObservers() throws Exception {
+        // Create two fake packages. One can access Type1, one can access Type2, they both can
+        // access Type3, and no one can access Type4.
+        final String fakePackage1 = "com.fake.listening.package1";
+
+        final String fakePackage2 = "com.fake.listening.package2";
+
+        final VisibilityChecker visibilityChecker =
+                (callerAccess, packageName, prefixedSchema, visibilityStore)
+                        -> {
+                    if (prefixedSchema.endsWith("Type1")) {
+                        return callerAccess.getCallingPackageName().equals(fakePackage1);
+                    } else if (prefixedSchema.endsWith("Type2")) {
+                        return callerAccess.getCallingPackageName().equals(fakePackage2);
+                    } else if (prefixedSchema.endsWith("Type3")) {
+                        return false;
+                    } else if (prefixedSchema.endsWith("Type4")) {
+                        return true;
+                    } else {
+                        throw new IllegalArgumentException(prefixedSchema);
+                    }
+                };
+        mAppSearchImpl.close();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                visibilityChecker);
+
+        // Add a schema.
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Type1").build(),
+                        new AppSearchSchema.Builder("Type2").build(),
+                        new AppSearchSchema.Builder("Type3").build(),
+                        new AppSearchSchema.Builder("Type4").build()
+                ),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Register three observers: one in each package, and another in package1 with a filter.
+        TestObserverCallback observerPkg1NoFilter = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakePackage1),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observerPkg1NoFilter);
+
+        TestObserverCallback observerPkg2NoFilter = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakePackage2),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                MoreExecutors.directExecutor(),
+                observerPkg2NoFilter);
+
+        TestObserverCallback observerPkg1FilterType4 = new TestObserverCallback();
+        mAppSearchImpl.registerObserverCallback(
+                new CallerAccess(/*callingPackageName=*/fakePackage1),
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().addFilterSchemas("Type4").build(),
+                MoreExecutors.directExecutor(),
+                observerPkg1FilterType4);
+
+        // Remove everything
+        mAppSearchImpl.setSchema(
+                mContext.getPackageName(),
+                "database1",
+                ImmutableList.of(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Dispatch notifications.
+        mAppSearchImpl.dispatchAndClearChangeNotifications();
+
+        // observerPkg1NoFilter should see Type1 and Type4 vanish.
+        // observerPkg2NoFilter should see Type2 and Type4 vanish.
+        // observerPkg2WithFilter should see Type4 vanish.
+        assertThat(observerPkg1NoFilter.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type1", "Type4"))
+        );
+        assertThat(observerPkg1NoFilter.getDocumentChanges()).isEmpty();
+
+        assertThat(observerPkg2NoFilter.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type2", "Type4"))
+        );
+        assertThat(observerPkg2NoFilter.getDocumentChanges()).isEmpty();
+
+        assertThat(observerPkg1FilterType4.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), "database1", ImmutableSet.of("Type4"))
+        );
+        assertThat(observerPkg1FilterType4.getDocumentChanges()).isEmpty();
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
index 75966bf..bf2ddbd 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -74,7 +74,8 @@
                 mTemporaryFolder.newFolder(),
                 new UnlimitedLimitConfig(),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         mLogger = new SimpleTestLogger();
     }
 
@@ -335,16 +336,18 @@
                 mTemporaryFolder.newFolder(),
                 new UnlimitedLimitConfig(),
                 initStatsBuilder,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         InitializeStats iStats = initStatsBuilder.build();
         appSearchImpl.close();
 
         assertThat(iStats).isNotNull();
+        // If the process goes really fast, the total latency could be 0. Since the default of total
+        // latency is also 0, we just remove the assert about NativeLatencyMillis.
         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);
@@ -363,7 +366,8 @@
                 folder,
                 new UnlimitedLimitConfig(),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE);
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
                 new AppSearchSchema.Builder("Type2").build());
@@ -371,9 +375,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -388,19 +390,21 @@
         // Create another appsearchImpl on the same folder
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+                folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         InitializeStats iStats = initStatsBuilder.build();
 
         assertThat(iStats).isNotNull();
+        // If the process goes really fast, the total latency could be 0. Since the default of total
+        // latency is also 0, we just remove the assert about NativeLatencyMillis.
         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.getSchemaTypeCount()).isEqualTo(4); // +2 for VisibilitySchema
         assertThat(iStats.hasReset()).isEqualTo(false);
         assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
         appSearchImpl.close();
@@ -413,7 +417,8 @@
         final File folder = mTemporaryFolder.newFolder();
 
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                folder, new UnlimitedLimitConfig(), /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
 
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
@@ -422,9 +427,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -447,7 +450,8 @@
         // Create another appsearchImpl on the same folder
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
         appSearchImpl = AppSearchImpl.create(
-                folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE);
+                folder, new UnlimitedLimitConfig(), initStatsBuilder, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
         InitializeStats iStats = initStatsBuilder.build();
 
         // Some of other fields are already covered by AppSearchImplTest#testReset()
@@ -475,9 +479,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -517,9 +519,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -558,9 +558,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -598,10 +596,11 @@
         SearchStats sStats = mLogger.mSearchStats;
 
         assertThat(sStats).isNotNull();
+        // If the process goes really fast, the total latency could be 0. Since the default of total
+        // latency is also 0, we just remove the assert about TotalLatencyMillis.
         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());
@@ -626,9 +625,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -664,9 +661,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -699,9 +694,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -739,9 +732,7 @@
                 testPackageName,
                 testDatabase,
                 schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -784,9 +775,7 @@
                 PACKAGE_NAME,
                 DATABASE,
                 Collections.singletonList(schema1),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -798,9 +787,7 @@
                 PACKAGE_NAME,
                 DATABASE,
                 Collections.singletonList(schema2),
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ sStatsBuilder);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
index 9df244e..4441f2a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
@@ -51,7 +51,8 @@
         mAppSearchImpl = AppSearchImpl.create(
                 mTemporaryFolder.newFolder(),
                 new UnlimitedLimitConfig(),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE);
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
     }
 
     @After
@@ -68,9 +69,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -95,12 +94,12 @@
                 searchSpec,
                 /*logger=*/ null);
 
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(1);
         assertThat(results.get(0).getGenericDocument()).isEqualTo(document1);
 
         // We get all documents, and it shouldn't fail if we keep calling getNextPage().
-        results = searchResults.getNextPage().get();
+        results = searchResults.getNextPageAsync().get();
         assertThat(results).isEmpty();
     }
 
@@ -113,9 +112,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
+                /*visibilityDocuments=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -146,18 +143,18 @@
                 searchSpec,
                 /*logger=*/ null);
         List<GenericDocument> outDocs = new ArrayList<>();
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(2);
         outDocs.add(results.get(0).getGenericDocument());
         outDocs.add(results.get(1).getGenericDocument());
 
-        results = searchResults.getNextPage().get();
+        results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(1);
         outDocs.add(results.get(0).getGenericDocument());
         assertThat(outDocs).containsExactly(document1, document2, document3);
 
         // We get all documents, and it shouldn't fail if we keep calling getNextPage().
-        results = searchResults.getNextPage().get();
+        results = searchResults.getNextPageAsync().get();
         assertThat(results).isEmpty();
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index 29aeb96..85f46c6 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -23,12 +23,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.annotation.NonNull;
-import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.testutil.AppSearchTestUtils;
 
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -37,13 +39,19 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 public class SearchSpecToProtoConverterTest {
+    /** An optimize strategy that always triggers optimize. */
+    public static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+    @Rule
+    public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
     @Test
     public void testToSearchSpecProto() throws Exception {
@@ -117,7 +125,7 @@
         assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
         assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
         assertThat(resultSpecProto.getSnippetSpec().getNumMatchesPerProperty()).isEqualTo(345);
-        assertThat(resultSpecProto.getSnippetSpec().getMaxWindowBytes()).isEqualTo(456);
+        assertThat(resultSpecProto.getSnippetSpec().getMaxWindowUtf32Length()).isEqualTo(456);
     }
 
     @Test
@@ -423,14 +431,20 @@
     }
 
     @Test
-    public void testRemoveInaccessibleSchemaFilter() {
-        String prefix = PrefixUtil.createPrefix("package", "database");
+    public void testRemoveInaccessibleSchemaFilter() throws Exception {
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(
+                mTemporaryFolder.newFolder(),
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/null,
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+        VisibilityStore visibilityStore = new VisibilityStore(appSearchImpl);
 
-        SearchSpec searchSpec = new SearchSpec.Builder().build();
-
+        final String prefix = PrefixUtil.createPrefix("package", "database");
         SchemaTypeConfigProto schemaTypeConfigProto =
                 SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
-        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                new SearchSpec.Builder().build(),
                 /*prefixes=*/ImmutableSet.of(prefix),
                 /*namespaceMap=*/ImmutableMap.of(
                         prefix, ImmutableSet.of("package$database/namespace1")),
@@ -441,27 +455,11 @@
                                 "package$database/schema3", schemaTypeConfigProto)));
 
         converter.removeInaccessibleSchemaFilter(
-                /*callerPackageName=*/"otherPackageName",
-                new VisibilityStore() {
-                    @Override
-                    public void setVisibility(@NonNull String packageName,
-                            @NonNull String databaseName,
-                            @NonNull Set<String> schemasNotDisplayedBySystem,
-                            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
-                            throws AppSearchException {
-
-                    }
-
-                    @Override
-                    public boolean isSchemaSearchableByCaller(@NonNull String packageName,
-                            @NonNull String databaseName, @NonNull String prefixedSchema,
-                            int callerUid, boolean callerHasSystemAccess) {
-                        // filter out schema 2 which is not searchable for user.
-                        return !prefixedSchema.equals(prefix + "schema2");
-                    }
-                },
-                /*callerUid=*/-1,
-                /*callerHasSystemAccess=*/true);
+                new CallerAccess(/*callingPackageName=*/"otherPackageName"),
+                visibilityStore,
+                AppSearchTestUtils.createMockVisibilityChecker(
+                        /*visiblePrefixedSchemas=*/ ImmutableSet.of(
+                                prefix + "schema1", prefix + "schema3")));
 
         SearchSpecProto searchSpecProto =
                 converter.toSearchSpecProto(/*queryExpression=*/"");
@@ -506,27 +504,9 @@
 
         // remove all target schema filter, and the query becomes nothing to search.
         nonEmptyConverter.removeInaccessibleSchemaFilter(
-                /*callerPackageName=*/"otherPackageName",
-                new VisibilityStore() {
-                    @Override
-                    public void setVisibility(@NonNull String packageName,
-                            @NonNull String databaseName,
-                            @NonNull Set<String> schemasNotDisplayedBySystem,
-                            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
-                            throws AppSearchException {
-
-                    }
-
-                    @Override
-                    public boolean isSchemaSearchableByCaller(@NonNull String packageName,
-                            @NonNull String databaseName, @NonNull String prefixedSchema,
-                            int callerUid, boolean callerHasSystemAccess) {
-                        // filter out all schema.
-                        return false;
-                    }
-                },
-                /*callerUid=*/-1,
-                /*callerHasSystemAccess=*/true);
+                new CallerAccess(/*callingPackageName=*/"otherPackageName"),
+                /*visibilityStore=*/null,
+                /*visibilityChecker=*/null);
         assertThat(nonEmptyConverter.isNothingToSearch()).isTrue();
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
new file mode 100644
index 0000000..ec88a88
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_NAME_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_PACKAGE_SCHEMA_TYPE;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_SHA_256_CERT_PROPERTY;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBILITY_SCHEMA_TYPE;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV0.DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+
+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;
+
+public class VisibilityStoreMigrationHelperFromV0Test {
+
+    /**
+     * 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 File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        // Give ourselves global query permissions
+        mFile = mTemporaryFolder.newFolder();
+    }
+
+    @Test
+    public void testVisibilityMigration_from0() throws Exception {
+        // Values for a "foo" client
+        String packageNameFoo = "packageFoo";
+        byte[] sha256CertFoo = new byte[32];
+
+        // Values for a "bar" client
+        String packageNameBar = "packageBar";
+        byte[] sha256CertBar = new byte[32];
+
+        // Create AppSearchImpl with visibility document version 0;
+        AppSearchImpl appSearchImplInV0 = buildAppSearchImplInV0();
+        // Build deprecated visibility documents in version 0
+        // "schema1" and "schema2" are platform hidden.
+        // "schema1" is accessible to packageFoo and "schema2" is accessible to packageBar.
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        GenericDocument deprecatedVisibilityToPackageFoo = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema1")
+                .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameFoo)
+                .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertFoo)
+                .build();
+        GenericDocument deprecatedVisibilityToPackageBar = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema2")
+                .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameBar)
+                .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertBar)
+                .build();
+        GenericDocument deprecatedVisibilityDocument = new GenericDocument.Builder<>(
+                VisibilityDocument.NAMESPACE,
+                VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
+                        "package", "database"),
+                DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                .setPropertyString(DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+                        prefix + "Schema1", prefix + "Schema2")
+                .setPropertyDocument(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+                        deprecatedVisibilityToPackageFoo, deprecatedVisibilityToPackageBar)
+                .build();
+
+        // Set some client schemas into AppSearchImpl with empty VisibilityDocument since we need to
+        // directly put old version of VisibilityDocument.
+        appSearchImplInV0.setSchema(
+                "package",
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("schema1").build(),
+                        new AppSearchSchema.Builder("schema2").build()),
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*schemaVersion=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Put deprecated visibility documents in version 0 to AppSearchImpl
+        appSearchImplInV0.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                deprecatedVisibilityDocument,
+                /*logger=*/null);
+
+        // Persist to disk and re-open the AppSearchImpl
+        appSearchImplInV0.close();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+
+        VisibilityDocument actualDocument1 = new VisibilityDocument(
+                appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.VISIBILITY_DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        /*id=*/ prefix + "Schema1",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        VisibilityDocument actualDocument2 = new VisibilityDocument(
+                appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.VISIBILITY_DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        /*id=*/ prefix + "Schema2",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+
+        VisibilityDocument expectedDocument1 =
+                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+                        .setNotDisplayedBySystem(true)
+                        .setCreationTimestampMillis(actualDocument1.getCreationTimestampMillis())
+                        .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
+                        .build();
+        VisibilityDocument expectedDocument2 =
+                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+                        .setNotDisplayedBySystem(true)
+                        .setCreationTimestampMillis(actualDocument2.getCreationTimestampMillis())
+                        .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
+                        .build();
+        assertThat(actualDocument1).isEqualTo(expectedDocument1);
+        assertThat(actualDocument2).isEqualTo(expectedDocument2);
+    }
+
+    /** Build AppSearchImpl with deprecated visibility schemas version 0.     */
+    private AppSearchImpl buildAppSearchImplInV0() throws Exception {
+        AppSearchSchema visibilityDocumentSchemaV0 = new AppSearchSchema.Builder(
+                DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+                        DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .build();
+        AppSearchSchema visibilityToPackagesSchemaV0 = new AppSearchSchema.Builder(
+                DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_PACKAGE_NAME_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                        DEPRECATED_SHA_256_CERT_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                        DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .build();
+        // Set deprecated visibility schema version 0 into AppSearchImpl.
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+        appSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                ImmutableList.of(visibilityDocumentSchemaV0, visibilityToPackagesSchemaV0),
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ true, // force push the old version into disk
+                /*version=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        return appSearchImpl;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
new file mode 100644
index 0000000..c85a7a9
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV1.DEPRECATED_ROLE_ASSISTANT;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStoreMigrationHelperFromV1.DEPRECATED_ROLE_HOME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+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.io.File;
+import java.util.Collections;
+
+public class VisibilityStoreMigrationHelperFromV1Test {
+
+    /**
+     * 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 File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        // Give ourselves global query permissions
+        mFile = mTemporaryFolder.newFolder();
+    }
+
+    @Test
+    public void testVisibilityMigration_from1() throws Exception {
+        // Values for a "foo" client
+        String packageNameFoo = "packageFoo";
+        byte[] sha256CertFoo = new byte[32];
+
+        // Values for a "bar" client
+        String packageNameBar = "packageBar";
+        byte[] sha256CertBar = new byte[32];
+
+        // Create AppSearchImpl with visibility document version 1;
+        AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+        appSearchImplInV1.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                ImmutableList.of(VisibilityDocumentV1.SCHEMA),
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ true, // force push the old version into disk
+                /*version=*/ 1,
+                /*setSchemaStatsBuilder=*/ null);
+        // Build deprecated visibility documents in version 1
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        VisibilityDocumentV1 visibilityDocumentV1 =
+                new VisibilityDocumentV1.Builder(prefix + "Schema")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
+                        .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
+                        .setVisibleToRoles(ImmutableSet.of(DEPRECATED_ROLE_HOME,
+                                DEPRECATED_ROLE_ASSISTANT))
+                        .setVisibleToPermissions(ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR))
+                        .build();
+
+        // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to
+        // directly put old version of VisibilityDocument.
+        appSearchImplInV1.setSchema(
+                "package",
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Schema").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*schemaVersion=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Put deprecated visibility documents in version 0 to AppSearchImpl
+        appSearchImplInV1.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                visibilityDocumentV1,
+                /*logger=*/null);
+
+        // Persist to disk and re-open the AppSearchImpl
+        appSearchImplInV1.close();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile, new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+
+        VisibilityDocument actualDocument = new VisibilityDocument(
+                appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.VISIBILITY_DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        /*id=*/ prefix + "Schema",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+
+        assertThat(actualDocument.isNotDisplayedBySystem()).isTrue();
+        assertThat(actualDocument.getPackageNames()).asList().containsExactly(packageNameFoo,
+                packageNameBar);
+        assertThat(actualDocument.getSha256Certs()).isEqualTo(
+                new byte[][] {sha256CertFoo, sha256CertBar});
+        assertThat(actualDocument.getVisibleToPermissions()).containsExactlyElementsIn(
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
new file mode 100644
index 0000000..d31247c
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.After;
+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;
+
+public class VisibilityStoreTest {
+
+    /**
+     * 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 File mAppSearchDir;
+    private AppSearchImpl mAppSearchImpl;
+    private VisibilityStore mVisibilityStore;
+
+    @Before
+    public void setUp() throws Exception {
+        mAppSearchDir = mTemporaryFolder.newFolder();
+        mAppSearchImpl = AppSearchImpl.create(
+                mAppSearchDir,
+                new UnlimitedLimitConfig(),
+                /*initStatsBuilder=*/ null,
+                ALWAYS_OPTIMIZE,
+                /*visibilityChecker=*/null);
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+    }
+
+    @After
+    public void tearDown() {
+        mAppSearchImpl.close();
+    }
+
+    /**
+     * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+     */
+    @Test
+    public void testValidPackageName() {
+        assertThat(VisibilityStore.VISIBILITY_PACKAGE_NAME)
+                .doesNotContain(String.valueOf(PrefixUtil.PACKAGE_DELIMITER));
+        assertThat(VisibilityStore.VISIBILITY_PACKAGE_NAME)
+                .doesNotContain(String.valueOf(PrefixUtil.DATABASE_DELIMITER));
+    }
+
+    /**
+     * Make sure that we don't conflict with any special characters that AppSearchImpl has reserved.
+     */
+    @Test
+    public void testValidDatabaseName() {
+        assertThat(VisibilityStore.VISIBILITY_DATABASE_NAME)
+                .doesNotContain(String.valueOf(PrefixUtil.PACKAGE_DELIMITER));
+        assertThat(VisibilityStore.VISIBILITY_DATABASE_NAME)
+                .doesNotContain(String.valueOf(PrefixUtil.DATABASE_DELIMITER));
+    }
+
+    @Test
+    public void testSetAndGetVisibility() throws Exception {
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityDocument);
+    }
+
+    @Test
+    public void testRemoveVisibility() throws Exception {
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+        assertThat(mVisibilityStore.getVisibility("Email"))
+                .isEqualTo(visibilityDocument);
+
+        mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityDocument.getId()));
+        assertThat(mVisibilityStore.getVisibility("Email")).isNull();
+    }
+
+    @Test
+    public void testRecoverBrokenVisibilitySchema() throws Exception {
+        // Create a broken schema which could be recovered to the latest schema in a compatible
+        // change. Since we won't set force override to true to recover the broken case.
+        AppSearchSchema brokenSchema = new AppSearchSchema.Builder(VisibilityDocument.SCHEMA_TYPE)
+                .build();
+
+        // Index a broken schema into AppSearch, use the latest version to make it broken.
+        mAppSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                Collections.singletonList(brokenSchema),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                /*setSchemaStatsBuilder=*/ null);
+        // Create VisibilityStore should recover the broken schema
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+        // We should be able to set and get Visibility settings.
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityDocument);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index 23b0e83..c7bc830 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java)
 package androidx.appsearch.localstorage;
 
 import androidx.annotation.NonNull;
@@ -33,7 +34,16 @@
         if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
             return true;
         }
-        if (Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER.equals(feature)) {
+        if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
+            return true;
+        }
+        if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
+            return true;
+        }
+        if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
+            return true;
+        }
+        if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
             return true;
         }
         return false;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index a0016a2..acaa566 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.app.AppSearchResult.RESULT_INTERNAL_ERROR;
+import static androidx.appsearch.app.AppSearchResult.RESULT_SECURITY_ERROR;
 import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
@@ -43,6 +45,7 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
 import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter;
@@ -58,8 +61,11 @@
 import androidx.appsearch.localstorage.stats.SearchStats;
 import androidx.appsearch.localstorage.stats.SetSchemaStats;
 import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.util.LogUtil;
 import androidx.collection.ArrayMap;
@@ -211,6 +217,21 @@
     private final ObserverManager mObserverManager = new ObserverManager();
 
     /**
+     * VisibilityStore will be used in {@link #setSchema} and {@link #getSchema} to store and query
+     * visibility information. But to create a {@link VisibilityStore}, it will call
+     * {@link #setSchema} and {@link #getSchema} to get the visibility schema. Make it nullable to
+     * avoid call it before we actually create it.
+     */
+    @Nullable
+    @VisibleForTesting
+    @GuardedBy("mReadWriteLock")
+    final VisibilityStore mVisibilityStoreLocked;
+
+    @Nullable
+    @GuardedBy("mReadWriteLock")
+    private final VisibilityChecker mVisibilityCheckerLocked;
+
+    /**
      * The counter to check when to call {@link #checkForOptimize}. The
      * interval is
      * {@link #CHECK_OPTIMIZE_INTERVAL}.
@@ -234,15 +255,20 @@
      * and putDocument.
      *
      * @param initStatsBuilder collects stats for initialization if provided.
+     * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has
+     *                          access to aa specific schema. Pass null will lost that ability and
+     *                          global querier could only get their own data.
      */
     @NonNull
     public static AppSearchImpl create(
             @NonNull File icingDir,
             @NonNull LimitConfig limitConfig,
             @Nullable InitializeStats.Builder initStatsBuilder,
-            @NonNull OptimizeStrategy optimizeStrategy)
+            @NonNull OptimizeStrategy optimizeStrategy,
+            @Nullable VisibilityChecker visibilityChecker)
             throws AppSearchException {
-        return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy);
+        return new AppSearchImpl(icingDir, limitConfig, initStatsBuilder, optimizeStrategy,
+                visibilityChecker);
     }
 
     /**
@@ -252,11 +278,13 @@
             @NonNull File icingDir,
             @NonNull LimitConfig limitConfig,
             @Nullable InitializeStats.Builder initStatsBuilder,
-            @NonNull OptimizeStrategy optimizeStrategy)
+            @NonNull OptimizeStrategy optimizeStrategy,
+            @Nullable VisibilityChecker visibilityChecker)
             throws AppSearchException {
         Preconditions.checkNotNull(icingDir);
         mLimitConfig = Preconditions.checkNotNull(limitConfig);
         mOptimizeStrategy = Preconditions.checkNotNull(optimizeStrategy);
+        mVisibilityCheckerLocked = visibilityChecker;
 
         mReadWriteLock.writeLock().lock();
         try {
@@ -355,6 +383,14 @@
                 resetLocked(initStatsBuilder);
             }
 
+            long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime();
+            mVisibilityStoreLocked = new VisibilityStore(this);
+            long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime();
+            if (initStatsBuilder != null) {
+                initStatsBuilder.setPrepareVisibilityStoreLatencyMillis((int)
+                        (prepareVisibilityStoreLatencyEndMillis
+                                - prepareVisibilityStoreLatencyStartMillis));
+            }
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -397,21 +433,21 @@
      *
      * <p>This method belongs to mutate group.
      *
-     * @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 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.
-     * @param version                       The overall version number of the request.
-     * @param setSchemaStatsBuilder         Builder for {@link SetSchemaStats} to hold stats for
-     *                                      setSchema
+     * @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 visibilityDocuments         {@link VisibilityDocument}s that contain all
+     *                                    visibility setting information for those schemas
+     *                                    has user custom settings. Other schemas in the list
+     *                                    that don't has a {@link VisibilityDocument}
+     *                                    will be treated as having the default visibility,
+     *                                    which is accessible by the system and no other 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.
+     * @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
@@ -423,160 +459,387 @@
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
-            @Nullable VisibilityStore visibilityStore,
-            @NonNull List<String> schemasNotDisplayedBySystem,
-            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages,
+            @NonNull List<VisibilityDocument> visibilityDocuments,
             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(schema, version);
-                newSchemaBuilder.addTypes(schemaTypeProto);
-            }
-
-            String prefix = createPrefix(packageName, databaseName);
-            // Combine the existing schema (which may have types from other prefixes) with this
-            // prefix's new schema. Modifies the existingSchemaBuilder.
-            RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(prefix,
-                    existingSchemaBuilder,
-                    newSchemaBuilder.build());
-
-            // Apply schema
-            SchemaProto finalSchema = existingSchemaBuilder.build();
-            mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
-            SetSchemaResultProto setSchemaResultProto =
-                    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) {
-                // 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.
-            for (SchemaTypeConfigProto schemaTypeConfigProto :
-                    rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
-                addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
-            }
-
-            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(
+            if (mObserverManager.isPackageObserved(packageName)) {
+                return doSetSchemaWithChangeNotificationLocked(
                         packageName,
                         databaseName,
-                        prefixedSchemasNotDisplayedBySystem,
-                        prefixedSchemasVisibleToPackages);
+                        schemas,
+                        visibilityDocuments,
+                        forceOverride,
+                        version,
+                        setSchemaStatsBuilder);
+            } else {
+                return doSetSchemaNoChangeNotificationLocked(
+                        packageName,
+                        databaseName,
+                        schemas,
+                        visibilityDocuments,
+                        forceOverride,
+                        version,
+                        setSchemaStatsBuilder);
             }
-
-            return SetSchemaResponseToProtoConverter
-                    .toSetSchemaResponse(setSchemaResultProto, prefix);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
     }
 
     /**
+     * Updates the AppSearch schema for this app, dispatching change notifications.
+     *
+     * @see #setSchema
+     * @see #doSetSchemaNoChangeNotificationLocked
+     */
+    @GuardedBy("mReadWriteLock")
+    @NonNull
+    private SetSchemaResponse doSetSchemaWithChangeNotificationLocked(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull List<AppSearchSchema> schemas,
+            @NonNull List<VisibilityDocument> visibilityDocuments,
+            boolean forceOverride,
+            int version,
+            @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
+        // First, capture the old state of the system. This includes the old schema as well as
+        // whether each registered observer can access each type. Once VisibilityStore is updated
+        // by the setSchema call, the information of which observers could see which types will be
+        // lost.
+        GetSchemaResponse oldSchema = getSchema(
+                packageName,
+                databaseName,
+                // A CallerAccess object for internal use that has local access to this database.
+                new CallerAccess(/*callingPackageName=*/packageName));
+
+        // Cache some lookup tables to help us work with the old schema
+        Set<AppSearchSchema> oldSchemaTypes = oldSchema.getSchemas();
+        Map<String, AppSearchSchema> oldSchemaNameToType = new ArrayMap<>(oldSchemaTypes.size());
+        // Maps unprefixed schema name to the set of listening packages that had visibility into
+        // that type under the old schema.
+        Map<String, Set<String>> oldSchemaNameToVisibleListeningPackage =
+                new ArrayMap<>(oldSchemaTypes.size());
+        for (AppSearchSchema oldSchemaType : oldSchemaTypes) {
+            String oldSchemaName = oldSchemaType.getSchemaType();
+            oldSchemaNameToType.put(oldSchemaName, oldSchemaType);
+            oldSchemaNameToVisibleListeningPackage.put(
+                    oldSchemaName,
+                    mObserverManager.getObserversForSchemaType(
+                            packageName,
+                            databaseName,
+                            oldSchemaName,
+                            mVisibilityStoreLocked,
+                            mVisibilityCheckerLocked));
+        }
+
+        // Apply the new schema
+        SetSchemaResponse setSchemaResponse = doSetSchemaNoChangeNotificationLocked(
+                packageName,
+                databaseName,
+                schemas,
+                visibilityDocuments,
+                forceOverride,
+                version,
+                setSchemaStatsBuilder);
+
+        // Cache some lookup tables to help us work with the new schema
+        Map<String, AppSearchSchema> newSchemaNameToType = new ArrayMap<>(schemas.size());
+        // Maps unprefixed schema name to the set of listening packages that have visibility into
+        // that type under the new schema.
+        Map<String, Set<String>> newSchemaNameToVisibleListeningPackage =
+                new ArrayMap<>(schemas.size());
+        for (AppSearchSchema newSchemaType : schemas) {
+            String newSchemaName = newSchemaType.getSchemaType();
+            newSchemaNameToType.put(newSchemaName, newSchemaType);
+            newSchemaNameToVisibleListeningPackage.put(
+                    newSchemaName,
+                    mObserverManager.getObserversForSchemaType(
+                            packageName,
+                            databaseName,
+                            newSchemaName,
+                            mVisibilityStoreLocked,
+                            mVisibilityCheckerLocked));
+        }
+
+        // Create a unified set of all schema names mentioned in either the old or new schema.
+        Set<String> allSchemaNames = new ArraySet<>(oldSchemaNameToType.keySet());
+        allSchemaNames.addAll(newSchemaNameToType.keySet());
+
+        // Perform the diff between the old and new schema.
+        for (String schemaName : allSchemaNames) {
+            final AppSearchSchema contentBefore = oldSchemaNameToType.get(schemaName);
+            final AppSearchSchema contentAfter = newSchemaNameToType.get(schemaName);
+
+            final boolean existBefore = (contentBefore != null);
+            final boolean existAfter = (contentAfter != null);
+
+            // This should never happen
+            if (!existBefore && !existAfter) {
+                continue;
+            }
+
+            boolean contentsChanged = true;
+            if (existBefore && existAfter && contentBefore.equals(contentAfter)) {
+                contentsChanged = false;
+            }
+
+            Set<String> oldVisibleListeners =
+                    oldSchemaNameToVisibleListeningPackage.get(schemaName);
+            Set<String> newVisibleListeners =
+                    newSchemaNameToVisibleListeningPackage.get(schemaName);
+            Set<String> allListeningPackages = new ArraySet<>(oldVisibleListeners);
+            if (newVisibleListeners != null) {
+                allListeningPackages.addAll(newVisibleListeners);
+            }
+
+            // Now that we've computed the relationship between the old and new schema, we go
+            // observer by observer and consider the observer's own personal view of the schema.
+            for (String listeningPackageName : allListeningPackages) {
+                // Figure out the visibility
+                final boolean visibleBefore = (
+                        existBefore
+                                && oldVisibleListeners != null
+                                && oldVisibleListeners.contains(listeningPackageName));
+                final boolean visibleAfter = (
+                        existAfter
+                                && newVisibleListeners != null
+                                && newVisibleListeners.contains(listeningPackageName));
+
+                // Now go through the truth table of all the relevant flags.
+                // visibleBefore and visibleAfter take into account existBefore and existAfter, so
+                // we can stop worrying about existBefore and existAfter.
+                boolean sendNotification = false;
+                if (visibleBefore && visibleAfter && contentsChanged) {
+                    sendNotification = true;  // Type configuration was modified
+                } else if (!visibleBefore && visibleAfter) {
+                    sendNotification = true;  // Newly granted visibility or type was created
+                } else if (visibleBefore && !visibleAfter) {
+                    sendNotification = true;  // Revoked visibility or type was deleted
+                } else {
+                    // No visibility before and no visibility after. Nothing to dispatch.
+                }
+
+                if (sendNotification) {
+                    mObserverManager.onSchemaChange(
+                            /*listeningPackageName=*/listeningPackageName,
+                            /*targetPackageName=*/packageName,
+                            /*databaseName=*/databaseName,
+                            /*schemaName=*/schemaName);
+                }
+            }
+        }
+
+        return setSchemaResponse;
+    }
+
+    /**
+     * Updates the AppSearch schema for this app, without dispatching change notifications.
+     *
+     * <p>This method can be used only when no one is observing {@code packageName}.
+     *
+     * @see #setSchema
+     * @see #doSetSchemaWithChangeNotificationLocked
+     */
+    @GuardedBy("mReadWriteLock")
+    @NonNull
+    private SetSchemaResponse doSetSchemaNoChangeNotificationLocked(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull List<AppSearchSchema> schemas,
+            @NonNull List<VisibilityDocument> visibilityDocuments,
+            boolean forceOverride,
+            int version,
+            @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
+        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(schema, version);
+            newSchemaBuilder.addTypes(schemaTypeProto);
+        }
+
+        String prefix = createPrefix(packageName, databaseName);
+        // Combine the existing schema (which may have types from other prefixes) with this
+        // prefix's new schema. Modifies the existingSchemaBuilder.
+        RewrittenSchemaResults rewrittenSchemaResults = rewriteSchema(prefix,
+                existingSchemaBuilder,
+                newSchemaBuilder.build());
+
+        // Apply schema
+        SchemaProto finalSchema = existingSchemaBuilder.build();
+        mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
+        SetSchemaResultProto setSchemaResultProto =
+                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) {
+            // 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.
+        for (SchemaTypeConfigProto schemaTypeConfigProto :
+                rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
+            addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+        }
+
+        for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
+            removeFromMap(mSchemaMapLocked, prefix, schemaType);
+        }
+        // Since the constructor of VisibilityStore will set schema. Avoid call visibility
+        // store before we have already created it.
+        if (mVisibilityStoreLocked != null) {
+            // Add prefix to all visibility documents.
+            List<VisibilityDocument> prefixedVisibilityDocuments =
+                    new ArrayList<>(visibilityDocuments.size());
+            // Find out which Visibility document is deleted or changed to all-default settings.
+            // We need to remove them from Visibility Store.
+            Set<String> deprecatedVisibilityDocuments =
+                    new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet());
+            for (int i = 0; i < visibilityDocuments.size(); i++) {
+                VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i);
+                // The VisibilityDocument is controlled by the client and it's untrusted but we
+                // make it safe by appending a prefix.
+                // We must control the package-database prefix. Therefore even if the client
+                // fake the id, they can only mess their own app. That's totally allowed and
+                // they can do this via the public API too.
+                String prefixedSchemaType = prefix + unPrefixedDocument.getId();
+                prefixedVisibilityDocuments.add(new VisibilityDocument(
+                        unPrefixedDocument.toBuilder()
+                                .setId(prefixedSchemaType)
+                                .build()));
+                // This schema has visibility settings. We should keep it from the removal list.
+                deprecatedVisibilityDocuments.remove(prefixedSchemaType);
+            }
+            // Now deprecatedVisibilityDocuments contains those existing schemas that has
+            // all-default visibility settings, add deleted schemas. That's all we need to
+            // remove.
+            deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes);
+            mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments);
+            mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments);
+        }
+        return SetSchemaResponseToProtoConverter
+                .toSetSchemaResponse(setSchemaResultProto, prefix);
+    }
+
+    /**
      * Retrieves the AppSearch schema 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.
+     * @param packageName  Package that owns the requested {@link AppSearchSchema} instances.
+     * @param databaseName Database that owns the requested {@link AppSearchSchema} instances.
+     * @param callerAccess Visibility access info of the calling app
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public GetSchemaResponse getSchema(@NonNull String packageName,
-            @NonNull String databaseName) throws AppSearchException {
+    public GetSchemaResponse getSchema(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull CallerAccess callerAccess)
+            throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
             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());
+                // Check that this type belongs to the requested app and that the caller has
+                // access to it.
+                SchemaTypeConfigProto typeConfig = fullSchema.getTypes(i);
+                String prefixedSchemaType = typeConfig.getSchemaType();
+                String typePrefix = getPrefix(prefixedSchemaType);
                 if (!prefix.equals(typePrefix)) {
+                    // This schema type doesn't belong to the database we're querying for.
                     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);
-                    }
+                if (!VisibilityUtil.isSchemaSearchableByCaller(
+                        callerAccess,
+                        packageName,
+                        prefixedSchemaType,
+                        mVisibilityStoreLocked,
+                        mVisibilityCheckerLocked)) {
+                    // Caller doesn't have access to this type.
+                    continue;
                 }
 
+                // Rewrite SchemaProto.types.schema_type
+                SchemaTypeConfigProto.Builder typeConfigBuilder = typeConfig.toBuilder();
+                PrefixUtil.removePrefixesFromSchemaType(typeConfigBuilder);
                 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.setVersion(typeConfig.getVersion());
                 responseBuilder.addSchema(schema);
+
+                // Populate visibility info. Since the constructor of VisibilityStore will get
+                // schema. Avoid call visibility store before we have already created it.
+                if (mVisibilityStoreLocked != null) {
+                    String typeName = typeConfig.getSchemaType().substring(typePrefix.length());
+                    VisibilityDocument visibilityDocument =
+                            mVisibilityStoreLocked.getVisibility(prefixedSchemaType);
+                    if (visibilityDocument != null) {
+                        if (visibilityDocument.isNotDisplayedBySystem()) {
+                            responseBuilder
+                                    .addSchemaTypeNotDisplayedBySystem(typeName);
+                        }
+                        String[] packageNames = visibilityDocument.getPackageNames();
+                        byte[][] sha256Certs = visibilityDocument.getSha256Certs();
+                        if (packageNames.length != sha256Certs.length) {
+                            throw new AppSearchException(RESULT_INTERNAL_ERROR,
+                                    "The length of package names and sha256Crets are different!");
+                        }
+                        if (packageNames.length != 0) {
+                            Set<PackageIdentifier> packageIdentifier = new ArraySet<>();
+                            for (int j = 0; j < packageNames.length; j++) {
+                                packageIdentifier.add(new PackageIdentifier(
+                                        packageNames[j], sha256Certs[j]));
+                            }
+                            responseBuilder.setSchemaTypeVisibleToPackages(typeName,
+                                    packageIdentifier);
+                        }
+                        Set<Set<Integer>> visibleToPermissions =
+                                visibilityDocument.getVisibleToPermissions();
+                        if (visibleToPermissions != null) {
+                            responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(
+                                    typeName,  visibleToPermissions);
+                        }
+                    }
+                }
             }
             return responseBuilder.build();
+
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -689,7 +952,13 @@
 
             // Prepare notifications
             mObserverManager.onDocumentChange(
-                    packageName, databaseName, document.getNamespace(), document.getSchemaType());
+                    packageName,
+                    databaseName,
+                    document.getNamespace(),
+                    document.getSchemaType(),
+                    document.getId(),
+                    mVisibilityStoreLocked,
+                    mVisibilityCheckerLocked);
         } finally {
             mReadWriteLock.writeLock().unlock();
 
@@ -758,6 +1027,67 @@
     }
 
     /**
+     * Retrieves a document from the AppSearch index by namespace and document ID from any
+     * application the caller is allowed to view
+     *
+     * <p>This method will handle both Icing engine errors as well as permission errors by
+     * throwing an obfuscated RESULT_NOT_FOUND exception. This is done so the caller doesn't
+     * receive information on whether or not a file they are not allowed to access exists or not.
+     * This is different from the behavior of {@link #getDocument}.
+     *
+     * @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.
+     * @param callerAccess      Visibility access info of the calling app
+     * @return The Document contents
+     * @throws AppSearchException on IcingSearchEngine error or invalid permissions
+     */
+    @NonNull
+    public GenericDocument globalGetDocument(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String namespace,
+            @NonNull String id,
+            @NonNull Map<String, List<String>> typePropertyPaths,
+            @NonNull CallerAccess callerAccess) throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+            // We retrieve the document before checking for access, as we do not know which
+            // schema the document is under. Schema is required for checking access
+            DocumentProto documentProto;
+            try {
+                documentProto = getDocumentProtoByIdLocked(packageName, databaseName,
+                        namespace, id, typePropertyPaths);
+
+                if (!VisibilityUtil.isSchemaSearchableByCaller(
+                        callerAccess,
+                        packageName,
+                        documentProto.getSchema(),
+                        mVisibilityStoreLocked,
+                        mVisibilityCheckerLocked)) {
+                    throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND);
+                }
+            } catch (AppSearchException e) {
+                throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
+                        "Document (" + namespace + ", " + id + ") not found.");
+            }
+
+            DocumentProto.Builder documentBuilder = documentProto.toBuilder();
+            removePrefixesFromDocument(documentBuilder);
+            String prefix = createPrefix(packageName, databaseName);
+            Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
+            return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
+                    prefix, schemaTypeMap);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /**
      * Retrieves a document from the AppSearch index by namespace and document ID.
      *
      * <p>This method belongs to query group.
@@ -773,47 +1103,24 @@
      */
     @NonNull
     public GenericDocument getDocument(
-            @NonNull String packageName, @NonNull String databaseName,
+            @NonNull String packageName,
+            @NonNull String databaseName,
             @NonNull String namespace,
             @NonNull String id,
             @NonNull Map<String, List<String>> typePropertyPaths) throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
             throwIfClosedLocked();
+            DocumentProto documentProto = getDocumentProtoByIdLocked(packageName, databaseName,
+                    namespace, id, typePropertyPaths);
+            DocumentProto.Builder documentBuilder = documentProto.toBuilder();
+            removePrefixesFromDocument(documentBuilder);
+
             String prefix = createPrefix(packageName, databaseName);
-            List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
-                    TypePropertyPathToProtoConverter
-                            .toTypePropertyMaskBuilderList(typePropertyPaths);
-            List<TypePropertyMask> prefixedPropertyMasks =
-                    new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
-            for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
-                String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
-                String prefixedType = nonPrefixedType.equals(
-                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
-                        ? nonPrefixedType : prefix + nonPrefixedType;
-                prefixedPropertyMasks.add(
-                        nonPrefixedPropertyMaskBuilders.get(i).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 {
@@ -822,6 +1129,58 @@
     }
 
     /**
+     * Returns a DocumentProto from Icing.
+     *
+     * @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 DocumentProto object
+     * @throws AppSearchException on IcingSearchEngine error
+     */
+    @NonNull
+    @GuardedBy("mReadWriteLock")
+    private DocumentProto getDocumentProtoByIdLocked(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String namespace,
+            @NonNull String id,
+            @NonNull Map<String, List<String>> typePropertyPaths)
+            throws AppSearchException {
+        String prefix = createPrefix(packageName, databaseName);
+        List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
+                TypePropertyPathToProtoConverter
+                        .toTypePropertyMaskBuilderList(typePropertyPaths);
+        List<TypePropertyMask> prefixedPropertyMasks =
+                new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
+        for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
+            String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
+            String prefixedType = nonPrefixedType.equals(
+                    GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
+                    ? nonPrefixedType : prefix + nonPrefixedType;
+            prefixedPropertyMasks.add(
+                    nonPrefixedPropertyMaskBuilders.get(i).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());
+
+        return getResultProto.getDocument();
+    }
+
+    /**
      * Executes a query against the AppSearch index and returns results.
      *
      * <p>This method belongs to query group.
@@ -897,16 +1256,10 @@
      *
      * <p>This method belongs to query group.
      *
-     * @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
+     * @param queryExpression Query String to search.
+     * @param searchSpec      Spec for setting filters, raw query etc.
+     * @param callerAccess    Visibility access info of the calling app
+     * @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.
@@ -915,10 +1268,7 @@
     public SearchResultPage globalQuery(
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
-            @NonNull String callerPackageName,
-            @Nullable VisibilityStore visibilityStore,
-            int callerUid,
-            boolean callerHasSystemAccess,
+            @NonNull CallerAccess callerAccess,
             @Nullable AppSearchLogger logger) throws AppSearchException {
         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
         SearchStats.Builder sStatsBuilder = null;
@@ -926,7 +1276,7 @@
             sStatsBuilder =
                     new SearchStats.Builder(
                             SearchStats.VISIBILITY_SCOPE_GLOBAL,
-                            callerPackageName);
+                            callerAccess.getCallingPackageName());
         }
 
         mReadWriteLock.readLock().lock();
@@ -952,10 +1302,10 @@
             }
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(searchSpec, prefixFilters, mNamespaceMapLocked,
-                    mSchemaMapLocked);
+                            mSchemaMapLocked);
             // Remove those inaccessible schemas.
-            searchSpecToProtoConverter.removeInaccessibleSchemaFilter(callerPackageName,
-                    visibilityStore, callerUid, callerHasSystemAccess);
+            searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
+                    callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
             if (searchSpecToProtoConverter.isNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
                 // empty SearchResult and skip sending request to Icing.
@@ -966,7 +1316,8 @@
                             queryExpression,
                             searchSpecToProtoConverter,
                             sStatsBuilder);
-            addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
+            addNextPageToken(
+                    callerAccess.getCallingPackageName(), searchResultPage.getNextPageToken());
             return searchResultPage;
         } finally {
             mReadWriteLock.readLock().unlock();
@@ -1266,7 +1617,14 @@
 
             // Prepare notifications
             if (schemaType != null) {
-                mObserverManager.onDocumentChange(packageName, databaseName, namespace, schemaType);
+                mObserverManager.onDocumentChange(
+                        packageName,
+                        databaseName,
+                        namespace,
+                        schemaType,
+                        documentId,
+                        mVisibilityStoreLocked,
+                        mVisibilityCheckerLocked);
             }
         } finally {
             mReadWriteLock.writeLock().unlock();
@@ -1360,9 +1718,9 @@
     /**
      * Executes removeByQuery, creating change notifications for removal.
      *
-     * @param packageName         The package name that owns the documents.
-     * @param finalSearchSpec     The final search spec that has been written through
-     *                            {@link SearchSpecToProtoConverter}.
+     * @param packageName             The package name that owns the documents.
+     * @param finalSearchSpec         The final search spec that has been written through
+     *                                {@link SearchSpecToProtoConverter}.
      * @param prefixedObservedSchemas The set of prefixed schemas that have valid registered
      *                                observers. Only changes to schemas in this set will be queued.
      */
@@ -1427,7 +1785,10 @@
                             packageName,
                             /*databaseName=*/ PrefixUtil.getDatabaseName(document.getNamespace()),
                             /*namespace=*/ PrefixUtil.removePrefix(document.getNamespace()),
-                            /*schemaType=*/ PrefixUtil.removePrefix(document.getSchema()));
+                            /*schemaType=*/ PrefixUtil.removePrefix(document.getSchema()),
+                            document.getUri(),
+                            mVisibilityStoreLocked,
+                            mVisibilityCheckerLocked);
                 }
             }
 
@@ -1771,12 +2132,13 @@
                     }
                     for (String databaseName : databaseNames) {
                         String removedPrefix = createPrefix(packageName, databaseName);
-                        mSchemaMapLocked.remove(removedPrefix);
+                        Map<String, SchemaTypeConfigProto> removedSchemas =
+                                mSchemaMapLocked.remove(removedPrefix);
+                        mVisibilityStoreLocked.removeVisibility(removedSchemas.keySet());
                         mNamespaceMapLocked.remove(removedPrefix);
                     }
                 }
             }
-            //TODO(b/145759910) clear visibility setting for package.
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -1951,7 +2313,7 @@
         synchronized (mNextPageTokensLocked) {
             Set<Long> nextPageTokens = mNextPageTokensLocked.get(packageName);
             if (nextPageTokens == null || !nextPageTokens.contains(nextPageToken)) {
-                throw new AppSearchException(AppSearchResult.RESULT_SECURITY_ERROR,
+                throw new AppSearchException(RESULT_SECURITY_ERROR,
                         "Package \"" + packageName + "\" cannot use nextPageToken: "
                                 + nextPageToken);
             }
@@ -1959,48 +2321,60 @@
     }
 
     /**
-     * Adds an {@link AppSearchObserverCallback} to monitor changes within the
-     * databases owned by {@code observedPackage} if they match the given
+     * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+     * {@code targetPackageName} if they match the given
      * {@link androidx.appsearch.observer.ObserverSpec}.
      *
-     * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} changes its schema visibility settings.
+     * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} changes its schema visibility settings.
      *
-     * <p>If no package matching {@code observedPackage} exists on the system, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} is installed and starts indexing data.
+     * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} is installed and starts indexing data.
      *
      * <p>Note that this method does not take the standard read/write lock that guards I/O, so it
      * will not queue behind I/O. Therefore it is safe to call from any thread including UI or
      * binder threads.
+     *
+     * @param listeningPackageAccess Visibility information about the app that wants to receive
+     *                               notifications.
+     * @param targetPackageName      The package that owns the data the observer wants to be
+     *                               notified for.
+     * @param spec                   Describes the kind of data changes the observer should trigger
+     *                               for.
+     * @param executor               The executor on which to trigger the observer callback to
+     *                               deliver notifications.
+     * @param observer               The callback to trigger on notifications.
      */
-    public void addObserver(
-            @NonNull String observedPackage,
+    public void registerObserverCallback(
+            @NonNull CallerAccess listeningPackageAccess,
+            @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull AppSearchObserverCallback observer) {
+            @NonNull ObserverCallback observer) {
         // This method doesn't consult mSchemaMap or mNamespaceMap, and it will register
         // observers for types that don't exist. This is intentional because we notify for types
         // being created or removed. If we only registered observer for existing types, it would
         // be impossible to ever dispatch a notification of a type being added.
-        mObserverManager.addObserver(observedPackage, spec, executor, observer);
+        mObserverManager.registerObserverCallback(
+                listeningPackageAccess, targetPackageName, spec, executor, observer);
     }
 
     /**
-     * Removes an {@link AppSearchObserverCallback} from watching the databases owned by
-     * {@code observedPackage}.
+     * Removes an {@link ObserverCallback} from watching the databases owned by
+     * {@code targetPackageName}.
      *
      * <p>All observers which compare equal to the given observer via
-     * {@link AppSearchObserverCallback#equals} are removed. This may be 0, 1, or many observers.
+     * {@link ObserverCallback#equals} are removed. This may be 0, 1, or many observers.
      *
      * <p>Note that this method does not take the standard read/write lock that guards I/O, so it
      * will not queue behind I/O. Therefore it is safe to call from any thread including UI or
      * binder threads.
      */
-    public void removeObserver(
-            @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
-        mObserverManager.removeObserver(observedPackage, observer);
+    public void unregisterObserverCallback(
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
+        mObserverManager.unregisterObserverCallback(targetPackageName, observer);
     }
 
     /**
@@ -2166,6 +2540,25 @@
     }
 
     /**
+     * Returns all prefixed schema types saved in AppSearch.
+     *
+     * <p>This method is inefficient to call repeatedly.
+     */
+    @NonNull
+    public List<String> getAllPrefixedSchemaTypes() {
+        mReadWriteLock.readLock().lock();
+        try {
+            List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
+            for (Map<String, SchemaTypeConfigProto> value : mSchemaMapLocked.values()) {
+                cachedPrefixedSchemaTypes.addAll(value.keySet());
+            }
+            return cachedPrefixedSchemaTypes;
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /**
      * Converts an erroneous status code from the Icing status enums to the AppSearchResult enums.
      *
      * <p>Callers should ensure that the status code is not OK or WARNING_DATA_LOSS.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
index 393f8f2..fe7bc77 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
@@ -165,7 +165,7 @@
      *                        {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}
      *                        added in.
      * @return  the {@link SetSchemaResponse} for this
-     *          {@link androidx.appsearch.app.AppSearchSession#setSchema} call.
+     *          {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync} call.
      *
      * @throws IOException        on i/o problem
      * @throws AppSearchException on AppSearch problem
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index 6c9e456..ef8ec3b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -16,24 +16,34 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
+
+import android.annotation.SuppressLint;
 import android.content.Context;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
 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.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -48,12 +58,12 @@
     private final Executor mExecutor;
     private final Features mFeatures;
     private final Context mContext;
+    @Nullable private final AppSearchLogger mLogger;
+
+    private final CallerAccess mSelfCallerAccess;
 
     private boolean mIsClosed = false;
 
-    @Nullable
-    private final AppSearchLogger mLogger;
-
     GlobalSearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull Executor executor,
@@ -65,6 +75,37 @@
         mFeatures = Preconditions.checkNotNull(features);
         mContext = Preconditions.checkNotNull(context);
         mLogger = logger;
+
+        mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mContext.getPackageName());
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull GetByDocumentIdRequest request) {
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        return FutureUtil.execute(mExecutor, () -> {
+            AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
+                    new AppSearchBatchResult.Builder<>();
+
+            Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+            CallerAccess access = new CallerAccess(mContext.getPackageName());
+            for (String id : request.getIds()) {
+                try {
+                    GenericDocument document = mAppSearchImpl.globalGetDocument(packageName,
+                            databaseName, request.getNamespace(), id, typePropertyPaths, access);
+                    resultBuilder.setSuccess(id, document);
+                } catch (Throwable t) {
+                    resultBuilder.setResult(id, throwableToFailedResult(t));
+                }
+            }
+            return resultBuilder.build();
+        });
     }
 
     @NonNull
@@ -92,7 +133,8 @@
      */
     @NonNull
     @Override
-    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+    public ListenableFuture<Void> reportSystemUsageAsync(
+            @NonNull ReportSystemUsageRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         return FutureUtil.execute(mExecutor, () -> {
@@ -102,6 +144,18 @@
         });
     }
 
+    @SuppressLint("KotlinPropertyAccess")
+    @NonNull
+    @Override
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync(
+            @NonNull String packageName, @NonNull String databaseName) {
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+        return FutureUtil.execute(mExecutor,
+                () -> mAppSearchImpl.getSchema(packageName, databaseName, mSelfCallerAccess));
+    }
+
     @NonNull
     @Override
     public Features getFeatures() {
@@ -109,36 +163,41 @@
     }
 
     @Override
-    public void addObserver(
-            @NonNull String observedPackage,
+    public void registerObserverCallback(
+            @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull AppSearchObserverCallback observer) {
-        Preconditions.checkNotNull(observedPackage);
+            @NonNull ObserverCallback observer) {
+        Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(spec);
         Preconditions.checkNotNull(executor);
         Preconditions.checkNotNull(observer);
         // LocalStorage does not support observing data from other packages.
-        if (!observedPackage.equals(mContext.getPackageName())) {
+        if (!targetPackageName.equals(mContext.getPackageName())) {
             throw new UnsupportedOperationException(
                     "Local storage implementation does not support receiving change notifications "
                             + "from other packages.");
         }
-        mAppSearchImpl.addObserver(observedPackage, spec, executor, observer);
+        mAppSearchImpl.registerObserverCallback(
+                /*listeningPackageAccess=*/mSelfCallerAccess,
+                /*targetPackageName=*/targetPackageName,
+                spec,
+                executor,
+                observer);
     }
 
     @Override
-    public void removeObserver(
-            @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
-        Preconditions.checkNotNull(observedPackage);
+    public void unregisterObserverCallback(
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
+        Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(observer);
         // LocalStorage does not support observing data from other packages.
-        if (!observedPackage.equals(mContext.getPackageName())) {
+        if (!targetPackageName.equals(mContext.getPackageName())) {
             throw new UnsupportedOperationException(
                     "Local storage implementation does not support receiving change notifications "
                             + "from other packages.");
         }
-        mAppSearchImpl.removeObserver(observedPackage, observer);
+        mAppSearchImpl.unregisterObserverCallback(targetPackageName, observer);
     }
 
     @Override
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 3ffb93d..6935b5e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -270,7 +270,7 @@
      *                {@link AppSearchSession}
      */
     @NonNull
-    public static ListenableFuture<AppSearchSession> createSearchSession(
+    public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull SearchContext context) {
         Preconditions.checkNotNull(context);
         return FutureUtil.execute(context.mExecutor, () -> {
@@ -281,6 +281,18 @@
     }
 
     /**
+     * @deprecated use {@link #createSearchSessionAsync}
+     * @param context The {@link SearchContext} contains all information to create a new
+     *                {@link AppSearchSession}
+     */
+    @NonNull
+    @Deprecated
+    public static ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull SearchContext context) {
+        return createSearchSessionAsync(context);
+    }
+
+    /**
      * Opens a new {@link GlobalSearchSession} on this storage.
      *
      * <p>This process requires a native search library. If it's not created, the initialization
@@ -290,7 +302,7 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
             @NonNull GlobalSearchContext context) {
         Preconditions.checkNotNull(context);
         return FutureUtil.execute(context.mExecutor, () -> {
@@ -301,6 +313,18 @@
     }
 
     /**
+     * @deprecated use {@link #createGlobalSearchSessionAsync}
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    @Deprecated
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        return createGlobalSearchSessionAsync(context);
+    }
+
+    /**
      * Returns the singleton instance of {@link LocalStorage}.
      *
      * <p>If the system is not initialized, it will be initialized using the provided
@@ -342,7 +366,8 @@
                 icingDir,
                 new UnlimitedLimitConfig(),
                 initStatsBuilder,
-                new JetpackOptimizeStrategy());
+                new JetpackOptimizeStrategy(),
+                /*visibilityChecker=*/null);
 
         if (logger != null) {
             initStatsBuilder.setTotalLatencyMillis(
@@ -382,7 +407,7 @@
                 mAppSearchImpl,
                 context.mExecutor,
                 new AlwaysSupportedFeatures(),
-                context.mContext.getPackageName(),
+                context.mContext,
                 context.mDatabaseName,
                 context.mLogger);
     }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
index 0d49147..c25be25 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
@@ -22,22 +22,29 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
 import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
- * Manages {@link AppSearchObserverCallback} instances and queues notifications to them for later
+ * Manages {@link ObserverCallback} instances and queues notifications to them for later
  * dispatch.
  *
  * <p>This class is thread-safe.
@@ -84,72 +91,94 @@
     }
 
     private static final class ObserverInfo {
+        /** The package which registered the observer. */
+        final CallerAccess mListeningPackageAccess;
         final ObserverSpec mObserverSpec;
         final Executor mExecutor;
-        final AppSearchObserverCallback mObserver;
-        volatile Set<DocumentChangeGroupKey> mDocumentChanges = new ArraySet<>();
+        final ObserverCallback mObserverCallback;
+        // Values is a set of document IDs
+        volatile Map<DocumentChangeGroupKey, Set<String>> mDocumentChanges = new ArrayMap<>();
+        // Keys are database prefixes, values are a set of schema names
+        volatile Map<String, Set<String>> mSchemaChanges = new ArrayMap<>();
 
         ObserverInfo(
+                @NonNull CallerAccess listeningPackageAccess,
                 @NonNull ObserverSpec observerSpec,
                 @NonNull Executor executor,
-                @NonNull AppSearchObserverCallback observer) {
+                @NonNull ObserverCallback observerCallback) {
+            mListeningPackageAccess = Preconditions.checkNotNull(listeningPackageAccess);
             mObserverSpec = Preconditions.checkNotNull(observerSpec);
             mExecutor = Preconditions.checkNotNull(executor);
-            mObserver = Preconditions.checkNotNull(observer);
+            mObserverCallback = Preconditions.checkNotNull(observerCallback);
         }
     }
 
     private final Object mLock = new Object();
 
-    /** Maps observed package to observer infos watching something in that package. */
+    /** Maps target packages to ObserverInfos watching something in that package. */
     @GuardedBy("mLock")
     private final Map<String, List<ObserverInfo>> mObserversLocked = new ArrayMap<>();
 
     private volatile boolean mHasNotifications = false;
 
     /**
-     * Adds an {@link AppSearchObserverCallback} to monitor changes within the
-     * databases owned by {@code observedPackage} if they match the given
+     * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+     * {@code targetPackageName} if they match the given
      * {@link androidx.appsearch.observer.ObserverSpec}.
      *
-     * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} changes its schema visibility settings.
+     * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} changes its schema visibility settings.
      *
-     * <p>If no package matching {@code observedPackage} exists on the system, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} is installed and starts indexing data.
+     * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} is installed and starts indexing data.
+     *
+     * <p>Note that this method does not take the standard read/write lock that guards I/O, so it
+     * will not queue behind I/O. Therefore it is safe to call from any thread including UI or
+     * binder threads.
+     *
+     * @param listeningPackageAccess Visibility information about the app that wants to receive
+     *                               notifications.
+     * @param targetPackageName      The package that owns the data the observerCallback wants to be
+     *                               notified for.
+     * @param spec                   Describes the kind of data changes the observerCallback should
+     *                               trigger for.
+     * @param executor               The executor on which to trigger the observerCallback callback
+     *                               to deliver notifications.
+     * @param observerCallback       The callback to trigger on notifications.
      */
-    public void addObserver(
-            @NonNull String observedPackage,
+    public void registerObserverCallback(
+            @NonNull CallerAccess listeningPackageAccess,
+            @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull AppSearchObserverCallback observer) {
+            @NonNull ObserverCallback observerCallback) {
         synchronized (mLock) {
-            List<ObserverInfo> infos = mObserversLocked.get(observedPackage);
+            List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
             if (infos == null) {
                 infos = new ArrayList<>();
-                mObserversLocked.put(observedPackage, infos);
+                mObserversLocked.put(targetPackageName, infos);
             }
-            infos.add(new ObserverInfo(spec, executor, observer));
+            infos.add(new ObserverInfo(listeningPackageAccess, spec, executor, observerCallback));
         }
     }
 
     /**
-     * Removes all observers that match via {@link AppSearchObserverCallback#equals} to the given
-     * observer from watching the observedPackage.
+     * Removes all observers that match via {@link ObserverCallback#equals} to the given observer
+     * from watching the targetPackageName.
      *
      * <p>Pending notifications queued for this observer, if any, are discarded.
      */
-    public void removeObserver(
-            @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
+    public void unregisterObserverCallback(
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer) {
         synchronized (mLock) {
-            List<ObserverInfo> infos = mObserversLocked.get(observedPackage);
+            List<ObserverInfo> infos = mObserversLocked.get(targetPackageName);
             if (infos == null) {
                 return;
             }
             for (int i = 0; i < infos.size(); i++) {
-                if (infos.get(i).mObserver.equals(observer)) {
+                if (infos.get(i).mObserverCallback.equals(observer)) {
                     infos.remove(i);
                     i--;
                 }
@@ -162,28 +191,105 @@
      *
      * <p>The notification will be queued in memory for later dispatch. You must call
      * {@link #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
+     *
+     * @param visibilityStore   Store for visibility information. If not provided, only access to
+     *                          own data will be allowed.
+     * @param visibilityChecker Checker for visibility access. If not provided, only access to own
+     *                          data will be allowed.
      */
     public void onDocumentChange(
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull String namespace,
-            @NonNull String schemaType) {
+            @NonNull String schemaType,
+            @NonNull String documentId,
+            @Nullable VisibilityStore visibilityStore,
+            @Nullable VisibilityChecker visibilityChecker) {
         synchronized (mLock) {
             List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
             if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
                 return; // No observers for this type
             }
             // Enqueue changes for later dispatch once the call returns
+            String prefixedSchema =
+                    PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
             DocumentChangeGroupKey key = null;
             for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
                 ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
-                if (matchesSpec(schemaType, observerInfo.mObserverSpec)) {
-                    if (key == null) {
-                        key = new DocumentChangeGroupKey(
-                                packageName, databaseName, namespace, schemaType);
-                    }
-                    observerInfo.mDocumentChanges.add(key);
+                if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
+                    continue;  // Observer doesn't want this notification
                 }
+                if (!VisibilityUtil.isSchemaSearchableByCaller(
+                        /*callerAccess=*/observerInfo.mListeningPackageAccess,
+                        /*targetPackageName=*/packageName,
+                        /*prefixedSchema=*/prefixedSchema,
+                        visibilityStore,
+                        visibilityChecker)) {
+                    continue;  // Observer can't have this notification.
+                }
+                if (key == null) {
+                    key = new DocumentChangeGroupKey(
+                            packageName, databaseName, namespace, schemaType);
+                }
+                Set<String> changedDocumentIds = observerInfo.mDocumentChanges.get(key);
+                if (changedDocumentIds == null) {
+                    changedDocumentIds = new ArraySet<>();
+                    observerInfo.mDocumentChanges.put(key, changedDocumentIds);
+                }
+                changedDocumentIds.add(documentId);
+            }
+            mHasNotifications = true;
+        }
+    }
+
+    /**
+     * Enqueues a change to a schema type for a single observer.
+     *
+     * <p>The notification will be queued in memory for later dispatch. You must call
+     * {@link #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
+     *
+     * <p>Note that unlike {@link #onDocumentChange}, the changes reported here are not dropped
+     * for observers that don't have visibility. This is because the observer might have had
+     * visibility before the schema change, and a final deletion needs to be sent to it. Caller
+     * is responsible for checking visibility of these notifications.
+     *
+     * @param listeningPackageName Name of package that subscribed to notifications and has been
+     *                             validated by the caller to have the right access to receive
+     *                             this notification.
+     * @param targetPackageName Name of package that owns the changed schema types.
+     * @param databaseName Database in which the changed schema types reside.
+     * @param schemaName Unprefixed name of the changed schema type.
+     */
+    public void onSchemaChange(
+            @NonNull String listeningPackageName,
+            @NonNull String targetPackageName,
+            @NonNull String databaseName,
+            @NonNull String schemaName) {
+        synchronized (mLock) {
+            List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(targetPackageName);
+            if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
+                return; // No observers for this type
+            }
+            // Enqueue changes for later dispatch once the call returns
+            String prefix = null;
+            for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+                ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+                if (!observerInfo.mListeningPackageAccess.getCallingPackageName()
+                        .equals(listeningPackageName)) {
+                    continue;  // Not the observer we've been requested to update right now.
+                }
+                if (!matchesSpec(schemaName, observerInfo.mObserverSpec)) {
+                    continue;  // Observer doesn't want this notification
+                }
+                if (prefix == null) {
+                    prefix = PrefixUtil.createPrefix(targetPackageName, databaseName);
+                }
+                Set<String> changedSchemaNames = observerInfo.mSchemaChanges.get(prefix);
+                if (changedSchemaNames == null) {
+                    changedSchemaNames = new ArraySet<>();
+                    observerInfo.mSchemaChanges.put(prefix, changedSchemaNames);
+                }
+                changedSchemaNames.add(schemaName);
             }
             mHasNotifications = true;
         }
@@ -216,6 +322,44 @@
         }
     }
 
+    /**
+     * Returns package names of listening packages registered for changes on the given
+     * {@code packageName}, {@code databaseName} and unprefixed {@code schemaType}, only if they
+     * have access to that type according to the provided {@code visibilityChecker}.
+     */
+    @NonNull
+    public Set<String> getObserversForSchemaType(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String schemaType,
+            @Nullable VisibilityStore visibilityStore,
+            @Nullable VisibilityChecker visibilityChecker) {
+        synchronized (mLock) {
+            List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
+            if (allObserverInfosForPackage == null) {
+                return Collections.emptySet();
+            }
+            Set<String> result = new ArraySet<>();
+            String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
+            for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+                ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+                if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
+                    continue;  // Observer doesn't want this notification
+                }
+                if (!VisibilityUtil.isSchemaSearchableByCaller(
+                        /*callerAccess=*/observerInfo.mListeningPackageAccess,
+                        /*targetPackageName=*/packageName,
+                        /*prefixedSchema=*/prefixedSchema,
+                        visibilityStore,
+                        visibilityChecker)) {
+                    continue;  // Observer can't have this notification.
+                }
+                result.add(observerInfo.mListeningPackageAccess.getCallingPackageName());
+            }
+            return result;
+        }
+    }
+
     /** Returns whether any notifications have been queued for dispatch. */
     public boolean hasNotifications() {
         return mHasNotifications;
@@ -243,28 +387,52 @@
     @GuardedBy("mLock")
     private void dispatchAndClearPendingNotificationsLocked(@NonNull ObserverInfo observerInfo) {
         // Get and clear the pending changes
-        Set<DocumentChangeGroupKey> documentChanges = observerInfo.mDocumentChanges;
-        if (documentChanges.isEmpty()) {
+        Map<String, Set<String>> schemaChanges = observerInfo.mSchemaChanges;
+        Map<DocumentChangeGroupKey, Set<String>> documentChanges = observerInfo.mDocumentChanges;
+        if (schemaChanges.isEmpty() && documentChanges.isEmpty()) {
             return;
         }
-        observerInfo.mDocumentChanges = new ArraySet<>();
+        if (!schemaChanges.isEmpty()) {
+            observerInfo.mSchemaChanges = new ArrayMap<>();
+        }
+        if (!documentChanges.isEmpty()) {
+            observerInfo.mDocumentChanges = new ArrayMap<>();
+        }
 
         // Dispatch the pending changes
         observerInfo.mExecutor.execute(() -> {
-            for (DocumentChangeGroupKey entry : documentChanges) {
-                // TODO(b/193494000): Buffer document URIs as the values of mDocumentChanges
-                // and include them in the final ChangeInfo
-                DocumentChangeInfo documentChangeInfo = new DocumentChangeInfo(
-                        entry.mPackageName,
-                        entry.mDatabaseName,
-                        entry.mNamespace,
-                        entry.mSchemaName);
+            // Schema changes
+            if (!schemaChanges.isEmpty()) {
+                for (Map.Entry<String, Set<String>> entry : schemaChanges.entrySet()) {
+                    SchemaChangeInfo schemaChangeInfo = new SchemaChangeInfo(
+                            /*packageName=*/PrefixUtil.getPackageName(entry.getKey()),
+                            /*databaseName=*/PrefixUtil.getDatabaseName(entry.getKey()),
+                            /*changedSchemaNames=*/entry.getValue());
 
-                try {
-                    // TODO(b/193494000): Add code to dispatch SchemaChangeInfo too.
-                    observerInfo.mObserver.onDocumentChanged(documentChangeInfo);
-                } catch (Throwable t) {
-                    Log.w(TAG, "AppSearchObserverCallback threw exception during dispatch", t);
+                    try {
+                        observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
+                    } catch (Throwable t) {
+                        Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                    }
+                }
+            }
+
+            // Document changes
+            if (!documentChanges.isEmpty()) {
+                for (Map.Entry<DocumentChangeGroupKey, Set<String>> entry
+                        : documentChanges.entrySet()) {
+                    DocumentChangeInfo documentChangeInfo = new DocumentChangeInfo(
+                            entry.getKey().mPackageName,
+                            entry.getKey().mDatabaseName,
+                            entry.getKey().mNamespace,
+                            entry.getKey().mSchemaName,
+                            entry.getValue());
+
+                    try {
+                        observerInfo.mObserverCallback.onDocumentChanged(documentChangeInfo);
+                    } catch (Throwable t) {
+                        Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                    }
                 }
             }
         });
@@ -280,12 +448,6 @@
     private static boolean matchesSpec(
             @NonNull String schemaType, @NonNull ObserverSpec observerSpec) {
         Set<String> schemaFilters = observerSpec.getFilterSchemas();
-        if (!schemaFilters.isEmpty() && !schemaFilters.contains(schemaType)) {
-            return false;
-        }
-        // TODO(b/193494000): We also need to check VisibilityStore to see if the observer is
-        //  allowed to access this type before granting access. Note if fixing this TODO makes the
-        //  method non-static we need to handle locking.
-        return true;
+        return schemaFilters.isEmpty() || schemaFilters.contains(schemaType);
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index 761ebfd..8da40bb 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -16,18 +16,15 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
-import android.os.Process;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.stats.SearchStats;
 import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -40,11 +37,13 @@
 
     private final Executor mExecutor;
 
-    // The package name to search over. If null, this will search over all package names.
-    @Nullable
+    /* The package name of the current app which is using the local backend. */
     private final String mPackageName;
 
-    // The database name to search over. If null, this will search over all database names.
+    /** A CallerAccess object describing local-only access of the current app. */
+    private final CallerAccess mSelfCallerAccess;
+
+    /* The database name to search over. If null, this will search over all database names. */
     @Nullable
     private final String mDatabaseName;
 
@@ -69,14 +68,15 @@
     SearchResultsImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull Executor executor,
-            @Nullable String packageName,
+            @NonNull String packageName,
             @Nullable String databaseName,
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
             @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
-        mPackageName = packageName;
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
         mDatabaseName = databaseName;
         mQueryExpression = Preconditions.checkNotNull(queryExpression);
         mSearchSpec = Preconditions.checkNotNull(searchSpec);
@@ -85,27 +85,17 @@
 
     @Override
     @NonNull
-    public ListenableFuture<List<SearchResult>> getNextPage() {
+    public ListenableFuture<List<SearchResult>> getNextPageAsync() {
         Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
         return FutureUtil.execute(mExecutor, () -> {
             SearchResultPage searchResultPage;
             if (mIsFirstLoad) {
                 mIsFirstLoad = false;
-                if (mPackageName == null) {
-                    throw new AppSearchException(
-                            AppSearchResult.RESULT_INVALID_ARGUMENT,
-                            "Invalid null package name for query");
-                } else if (mDatabaseName == null) {
+                if (mDatabaseName == null) {
                     mVisibilityScope = SearchStats.VISIBILITY_SCOPE_GLOBAL;
                     // Global queries aren't restricted to a single database
                     searchResultPage = mAppSearchImpl.globalQuery(
-                            mQueryExpression,
-                            mSearchSpec,
-                            mPackageName,
-                            /*visibilityStore=*/ null,
-                            Process.myUid(),
-                            /*callerHasSystemAccess=*/ false,
-                            mLogger);
+                            mQueryExpression, mSearchSpec, mSelfCallerAccess, mLogger);
                 } else {
                     mVisibilityScope = SearchStats.VISIBILITY_SCOPE_LOCAL;
                     // Normal local query, pass in specified database.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index a695642..8d3ebb1 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -18,6 +18,7 @@
 
 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
 
+import android.content.Context;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -31,7 +32,6 @@
 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.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportUsageRequest;
@@ -40,14 +40,15 @@
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityDocument;
 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.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.util.SchemaMigrationUtil;
-import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -70,49 +71,50 @@
  */
 class SearchSessionImpl implements AppSearchSession {
     private static final String TAG = "AppSearchSessionImpl";
+
     private final AppSearchImpl mAppSearchImpl;
     private final Executor mExecutor;
     private final Features mFeatures;
-    private final String mPackageName;
+    private final Context mContext;
     private final String mDatabaseName;
+    @Nullable private final AppSearchLogger mLogger;
+
+    private final String mPackageName;
+    private final CallerAccess mSelfCallerAccess;
+
     private volatile boolean mIsMutated = false;
     private volatile boolean mIsClosed = false;
-    @Nullable private final AppSearchLogger mLogger;
 
     SearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull Executor executor,
             @NonNull Features features,
-            @NonNull String packageName,
+            @NonNull Context context,
             @NonNull String databaseName,
             @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
         mFeatures = Preconditions.checkNotNull(features);
-        mPackageName = packageName;
+        mContext = Preconditions.checkNotNull(context);
         mDatabaseName = Preconditions.checkNotNull(databaseName);
         mLogger = logger;
+
+        mPackageName = mContext.getPackageName();
+        mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
     }
 
     @Override
     @NonNull
-    public ListenableFuture<SetSchemaResponse> setSchema(
+    public ListenableFuture<SetSchemaResponse> setSchemaAsync(
             @NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
 
         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()));
-            }
+            // Extract a Map<schema, VisibilityDocument> from the request.
+            List<VisibilityDocument> visibilityDocuments = VisibilityDocument
+                    .toVisibilityDocuments(request);
 
             SetSchemaStats.Builder setSchemaStatsBuilder = null;
             if (mLogger != null) {
@@ -123,20 +125,25 @@
             // No need to trigger migration if user never set migrator.
             if (migrators.size() == 0) {
                 SetSchemaResponse setSchemaResponse =
-                        setSchemaNoMigrations(request, copySchemasVisibleToPackages,
-                                setSchemaStatsBuilder);
+                        setSchemaNoMigrations(request, visibilityDocuments, setSchemaStatsBuilder);
+
+                // Schedule a task to dispatch change notifications. See requirements for where the
+                // method is called documented in the method description.
+                dispatchChangeNotifications();
+
                 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);
+            GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                    mPackageName, mDatabaseName, mSelfCallerAccess);
             int currentVersion = getSchemaResponse.getVersion();
             int finalVersion = request.getVersion();
             Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
@@ -144,8 +151,7 @@
             // No need to trigger migration if no migrator is active.
             if (activeMigrators.size() == 0) {
                 SetSchemaResponse setSchemaResponse =
-                        setSchemaNoMigrations(request, copySchemasVisibleToPackages,
-                                setSchemaStatsBuilder);
+                        setSchemaNoMigrations(request, visibilityDocuments, setSchemaStatsBuilder);
                 if (setSchemaStatsBuilder != null) {
                     setSchemaStatsBuilder.setTotalLatencyMillis(
                             (int) (SystemClock.elapsedRealtime() - startMillis));
@@ -161,9 +167,7 @@
                     mPackageName,
                     mDatabaseName,
                     new ArrayList<>(request.getSchemas()),
-                    /*visibilityStore=*/ null,
-                    new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
-                    copySchemasVisibleToPackages,
+                    visibilityDocuments,
                     /*forceOverride=*/false,
                     request.getVersion(),
                     setSchemaStatsBuilder);
@@ -197,9 +201,7 @@
                             mPackageName,
                             mDatabaseName,
                             new ArrayList<>(request.getSchemas()),
-                            /*visibilityStore=*/ null,
-                            new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
-                            copySchemasVisibleToPackages,
+                            visibilityDocuments,
                             /*forceOverride=*/ true,
                             request.getVersion(),
                             setSchemaStatsBuilder);
@@ -214,6 +216,10 @@
                         migrationHelper.readAndPutDocuments(responseBuilder,
                                 schemaMigrationStatsBuilder);
 
+                // Schedule a task to dispatch change notifications. See requirements for where the
+                // method is called documented in the method description.
+                dispatchChangeNotifications();
+
                 if (schemaMigrationStatsBuilder != null) {
                     long endMillis = SystemClock.elapsedRealtime();
                     schemaMigrationStatsBuilder
@@ -251,14 +257,15 @@
 
     @Override
     @NonNull
-    public ListenableFuture<GetSchemaResponse> getSchema() {
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName));
+        return execute(
+                () -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName, mSelfCallerAccess));
     }
 
     @NonNull
     @Override
-    public ListenableFuture<Set<String>> getNamespaces() {
+    public ListenableFuture<Set<String>> getNamespacesAsync() {
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             List<String> namespaces = mAppSearchImpl.getNamespaces(mPackageName, mDatabaseName);
@@ -268,7 +275,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -303,7 +310,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
             @NonNull GetByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -346,7 +353,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+    public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
@@ -364,7 +371,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
             @NonNull RemoveByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
@@ -403,7 +410,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> remove(
+    public ListenableFuture<Void> removeAsync(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
@@ -432,14 +439,14 @@
 
     @Override
     @NonNull
-    public ListenableFuture<StorageInfo> getStorageInfo() {
+    public ListenableFuture<StorageInfo> getStorageInfoAsync() {
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> mAppSearchImpl.getStorageInfoForDatabase(mPackageName, mDatabaseName));
     }
 
     @NonNull
     @Override
-    public ListenableFuture<Void> requestFlush() {
+    public ListenableFuture<Void> requestFlushAsync() {
         return execute(() -> {
             mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
             return null;
@@ -472,20 +479,18 @@
     /**
      * Set schema to Icing for no-migration scenario.
      *
-     * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
+     * <p>We only need one time {@link #setSchemaAsync} call for no-migration scenario by using the
      * forceoverride in the request.
      */
     private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
-            @NonNull Map<String, List<PackageIdentifier>> copySchemasVisibleToPackages,
+            @NonNull List<VisibilityDocument> visibilityDocuments,
             SetSchemaStats.Builder setSchemaStatsBuilder)
             throws AppSearchException {
         SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
                 mPackageName,
                 mDatabaseName,
                 new ArrayList<>(request.getSchemas()),
-                /*visibilityStore=*/ null,
-                new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
-                copySchemasVisibleToPackages,
+                visibilityDocuments,
                 request.isForceOverride(),
                 request.getVersion(),
                 setSchemaStatsBuilder);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index af24c98..e3c0a7da 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -17,7 +17,6 @@
 package androidx.appsearch.localstorage.converter;
 
 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.removePrefix;
 
@@ -28,7 +27,10 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -152,36 +154,30 @@
      * For each target schema, we will check visibility store is that accessible to the caller. And
      * remove this schemas if it is not allowed for caller to query.
      *
-     * @param callerPackageName            The package name of caller
-     * @param visibilityStore              The visibility store which holds all visibility settings.
-     *                                     If you pass null, all schemas that don't belong to the
-     *                                     caller package will be removed.
-     * @param callerUid                    The uid of the caller.
-     * @param callerHasSystemAccess        Whether the caller has system access.
+     * @param callerAccess      Visibility access info of the calling app
+     * @param visibilityStore   The {@link VisibilityStore} that store all visibility
+     *                          information.
+     * @param visibilityChecker Optional visibility checker to check whether the caller
+     *                          could access target schemas. Pass {@code null} will
+     *                          reject access for all documents which doesn't belong
+     *                          to the calling package.
      */
-    public void removeInaccessibleSchemaFilter(@NonNull String callerPackageName,
+    public void removeInaccessibleSchemaFilter(
+            @NonNull CallerAccess callerAccess,
             @Nullable VisibilityStore visibilityStore,
-            int callerUid,
-            boolean callerHasSystemAccess) {
+            @Nullable VisibilityChecker visibilityChecker) {
         Iterator<String> targetPrefixedSchemaFilterIterator =
                 mTargetPrefixedSchemaFilters.iterator();
         while (targetPrefixedSchemaFilterIterator.hasNext()) {
             String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
             String packageName = getPackageName(targetPrefixedSchemaFilter);
 
-            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(targetPrefixedSchemaFilter);
-                allow = visibilityStore.isSchemaSearchableByCaller(packageName, databaseName,
-                        targetPrefixedSchemaFilter, callerUid, callerHasSystemAccess);
-            }
-            if (!allow) {
+            if (!VisibilityUtil.isSchemaSearchableByCaller(
+                    callerAccess,
+                    packageName,
+                    targetPrefixedSchemaFilter,
+                    visibilityStore,
+                    visibilityChecker)) {
                 targetPrefixedSchemaFilterIterator.remove();
             }
         }
@@ -229,7 +225,7 @@
                         ResultSpecProto.SnippetSpecProto.newBuilder()
                                 .setNumToSnippet(mSearchSpec.getSnippetCount())
                                 .setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
-                                .setMaxWindowBytes(mSearchSpec.getMaxSnippetSize()));
+                                .setMaxWindowUtf32Length(mSearchSpec.getMaxSnippetSize()));
 
         // Rewrites the typePropertyMasks that exist in {@code prefixes}.
         int groupingType = mSearchSpec.getResultGroupingTypeFlags();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
index e3abf90..101ef1f 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
@@ -56,6 +56,7 @@
             CALL_TYPE_GLOBAL_SEARCH,
             CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH,
             CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH,
+            CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CallType {
@@ -76,6 +77,7 @@
     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;
+    public static final int CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID = 15;
 
     @Nullable
     private final String mPackageName;
@@ -145,11 +147,11 @@
      * Returns number of operations succeeded.
      *
      * <p>For example, for
-     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * {@link androidx.appsearch.app.AppSearchSession#putAsync}, 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 androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
      * {@link CallStats#getNumOperationsSucceeded()} and
      * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
      * operation.
@@ -162,11 +164,12 @@
      * Returns number of operations failed.
      *
      * <p>For example, for
-     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * {@link androidx.appsearch.app.AppSearchSession#putAsync}, 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
+     * <p>For non-batch calls such as
+     * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
+     * {@link CallStats#getNumOperationsSucceeded()} and
      * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
      * operation.
      */
@@ -235,12 +238,12 @@
          * Sets number of operations succeeded.
          *
          * <p>For example, for
-         * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of
+         * {@link androidx.appsearch.app.AppSearchSession#putAsync}, 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 androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
          * {@link CallStats#getNumOperationsSucceeded()} and
          * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
          * operation.
@@ -254,12 +257,12 @@
         /**
          * Sets number of operations failed.
          *
-         * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#put}, it is the
+         * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#putAsync}, 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 androidx.appsearch.app.AppSearchSession#setSchemaAsync}, the sum of
          * {@link CallStats#getNumOperationsSucceeded()} and
          * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
          * operation.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
index c8bc687..e9a25fd 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -23,7 +23,7 @@
 
 /**
  * A class for holding detailed stats to log for each individual document put by a
- * {@link androidx.appsearch.app.AppSearchSession#put} call.
+ * {@link androidx.appsearch.app.AppSearchSession#putAsync} call.
  *
  * @hide
  */
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
index 3a63aa0..7eb4820 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
@@ -29,8 +29,8 @@
 
 /**
  * Class holds detailed stats for
- * {@link androidx.appsearch.app.AppSearchSession#remove(RemoveByDocumentIdRequest)} and
- * {@link androidx.appsearch.app.AppSearchSession#remove(String, SearchSpec)}
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync(RemoveByDocumentIdRequest)} and
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync(String, SearchSpec)}
  *
  * @hide
  */
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
index 12d4587..cf0bde3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
@@ -25,10 +25,13 @@
 import androidx.appsearch.exceptions.AppSearchException;
 
 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;
 
 /**
  * Provides utility functions for working with package + database prefixes.
+ *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -41,7 +44,8 @@
     @VisibleForTesting
     public static final char PACKAGE_DELIMITER = '$';
 
-    private PrefixUtil() {}
+    private PrefixUtil() {
+    }
 
     /**
      * Creates prefix string for given package name and database name.
@@ -50,6 +54,7 @@
     public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
         return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
     }
+
     /**
      * Creates prefix string for given package name.
      */
@@ -231,4 +236,37 @@
 
         return schemaPrefix;
     }
+
+    /**
+     * Removes any prefixes from types mentioned anywhere in {@code typeConfigBuilder}.
+     *
+     * @param typeConfigBuilder The schema type to mutate
+     * @return Prefix name that was removed from the schema type.
+     * @throws AppSearchException if there are unexpected database prefixing errors.
+     */
+    @NonNull
+    public static String removePrefixesFromSchemaType(
+            @NonNull SchemaTypeConfigProto.Builder typeConfigBuilder)
+            throws AppSearchException {
+        String typePrefix = PrefixUtil.getPrefix(typeConfigBuilder.getSchemaType());
+        // Rewrite SchemaProto.types.schema_type
+        String newSchemaType =
+                typeConfigBuilder.getSchemaType().substring(typePrefix.length());
+        typeConfigBuilder.setSchemaType(newSchemaType);
+
+        // Rewrite SchemaProto.types.properties.schema_type
+        for (int propertyIdx = 0;
+                propertyIdx < typeConfigBuilder.getPropertiesCount();
+                propertyIdx++) {
+            if (!typeConfigBuilder.getProperties(propertyIdx).getSchemaType().isEmpty()) {
+                PropertyConfigProto.Builder propertyConfigBuilder =
+                        typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+                String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+                        .substring(typePrefix.length());
+                propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+                typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+            }
+        }
+        return typePrefix;
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
new file mode 100644
index 0000000..1a47109
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Contains attributes of an API caller relevant to its access via visibility store.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CallerAccess {
+    private final String mCallingPackageName;
+
+    /**
+     * Constructs a new {@link CallerAccess}.
+     *
+     * @param callingPackageName    The name of the package which wants to access data.
+     */
+    public CallerAccess(@NonNull String callingPackageName) {
+        mCallingPackageName = Preconditions.checkNotNull(callingPackageName);
+    }
+
+    /** Returns the name of the package which wants to access data. */
+    @NonNull
+    public String getCallingPackageName() {
+        return mCallingPackageName;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (!(o instanceof CallerAccess)) return false;
+        CallerAccess that = (CallerAccess) o;
+        return mCallingPackageName.equals(that.mCallingPackageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return mCallingPackageName.hashCode();
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
new file mode 100644
index 0000000..7801562
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * An interface for classes that validate document visibility data.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface VisibilityChecker {
+    /**
+     * Checks whether the given caller has access to the given prefixed schemas.
+     *
+     * @param callerAccess      Visibility access info of the calling app
+     * @param packageName Package of app that owns the schemas.
+     * @param prefixedSchema The prefixed schema type that the caller want to access.
+     * @param visibilityStore The {@link VisibilityStore} that store all visibility information.
+     */
+    boolean isSchemaSearchableByCaller(
+            @NonNull CallerAccess callerAccess,
+            @NonNull String packageName,
+            @NonNull String prefixedSchema,
+            @NonNull VisibilityStore visibilityStore);
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
new file mode 100644
index 0000000..537fd81
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.Set;
+
+/**
+ * Holds the visibility settings in version 1 that apply to a schema type.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class VisibilityDocumentV1 extends GenericDocument {
+    /**
+     * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+     */
+    static final String SCHEMA_TYPE = "VisibilityType";
+    /** Namespace of documents that contain visibility settings */
+    static final String NAMESPACE = "";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+    /** Property that holds the package name that can access a schema. */
+    private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+    /** Property that holds the role can access a schema. */
+    private static final String ROLE_PROPERTY = "role";
+
+    /** Property that holds the required permissions to access the schema. */
+    private static final String PERMISSION_PROPERTY = "permission";
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     */
+    static final AppSearchSchema
+            SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                    NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.LongPropertyConfig.Builder(ROLE_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.LongPropertyConfig.Builder(PERMISSION_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .build();
+
+    VisibilityDocumentV1(@NonNull GenericDocument genericDocument) {
+        super(genericDocument);
+    }
+
+    /** Returns whether this schema is visible to the system. */
+    boolean isNotDisplayedBySystem() {
+        return getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+    }
+
+    /**
+     * Returns a package name array which could access this schema. Use {@link #getSha256Certs()}
+     * to get package's sha 256 certs. The same index of package names array and sha256Certs array
+     * represents same package.
+     */
+    @NonNull
+    String[] getPackageNames() {
+        return getPropertyStringArray(PACKAGE_NAME_PROPERTY);
+    }
+
+    /**
+     * Returns a package sha256Certs array which could access this schema. Use
+     * {@link #getPackageNames()} to get package's name. The same index of package names array
+     * and sha256Certs array represents same package.
+     */
+    @NonNull
+    byte[][] getSha256Certs() {
+        return getPropertyBytesArray(SHA_256_CERT_PROPERTY);
+    }
+
+    /**
+     * Returns an array of Android Roles that have access to the schema this
+     * {@link VisibilityDocumentV1} represents.
+     */
+    @Nullable
+    Set<Integer> getVisibleToRoles() {
+        return toInts(getPropertyLongArray(ROLE_PROPERTY));
+    }
+
+    /**
+     * Returns an array of Android Permissions that caller mush hold to access the schema
+     * this {@link VisibilityDocumentV1} represents.
+     */
+    @Nullable
+    Set<Integer> getVisibleToPermissions() {
+        return toInts(getPropertyLongArray(PERMISSION_PROPERTY));
+    }
+
+    /** Builder for {@link VisibilityDocumentV1}. */
+    static class Builder extends GenericDocument.Builder<Builder> {
+        private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
+
+        /**
+         * Creates a {@link Builder} for a {@link VisibilityDocumentV1}.
+         *
+         * @param id The SchemaType of the {@link AppSearchSchema} that this
+         *           {@link VisibilityDocumentV1} represents. The package and database prefix will
+         *           be added in server side. We are using prefixed schema type to be the final
+         *           id of this {@link VisibilityDocumentV1}.
+         */
+        Builder(@NonNull String id) {
+            super(NAMESPACE, id, SCHEMA_TYPE);
+        }
+
+        /** Sets whether this schema has opted out of platform surfacing. */
+        @NonNull
+        Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+            return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+                    notDisplayedBySystem);
+        }
+
+        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @NonNull
+        Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
+            Preconditions.checkNotNull(packageIdentifiers);
+            mPackageIdentifiers.addAll(packageIdentifiers);
+            return this;
+        }
+
+        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @NonNull
+        Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+            Preconditions.checkNotNull(packageIdentifier);
+            mPackageIdentifiers.add(packageIdentifier);
+            return this;
+        }
+
+        /** Add a set of Android role that has access to the schema this
+         * {@link VisibilityDocumentV1} represents. */
+        @NonNull
+        Builder setVisibleToRoles(@NonNull Set<Integer> visibleToRoles) {
+            Preconditions.checkNotNull(visibleToRoles);
+            setPropertyLong(ROLE_PROPERTY, toLongs(visibleToRoles));
+            return this;
+        }
+
+        /** Add a set of Android role that has access to the schema this
+         * {@link VisibilityDocumentV1} represents. */
+        @NonNull
+        Builder setVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
+            Preconditions.checkNotNull(visibleToPermissions);
+            setPropertyLong(PERMISSION_PROPERTY, toLongs(visibleToPermissions));
+            return this;
+        }
+
+        /** Build a {@link VisibilityDocumentV1} */
+        @Override
+        @NonNull
+        public VisibilityDocumentV1 build() {
+            String[] packageNames = new String[mPackageIdentifiers.size()];
+            byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];
+            int i = 0;
+            for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {
+                packageNames[i] = packageIdentifier.getPackageName();
+                sha256Certs[i] = packageIdentifier.getSha256Certificate();
+                ++i;
+            }
+            setPropertyString(PACKAGE_NAME_PROPERTY, packageNames);
+            setPropertyBytes(SHA_256_CERT_PROPERTY, sha256Certs);
+            return new VisibilityDocumentV1(super.build());
+        }
+    }
+
+    @NonNull
+    static long[] toLongs(@NonNull Set<Integer> properties) {
+        long[] outputs = new long[properties.size()];
+        int i = 0;
+        for (int property : properties) {
+            outputs[i++] = property;
+        }
+        return outputs;
+    }
+
+    @Nullable
+    private static Set<Integer> toInts(@Nullable long[] properties) {
+        if (properties == null) {
+            return null;
+        }
+        Set<Integer> outputs = new ArraySet<>(properties.length);
+        for (long property : properties) {
+            outputs.add((int) property);
+        }
+        return outputs;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
index d3d754e..79e22ff5c 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
@@ -15,60 +15,246 @@
  */
 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 static androidx.appsearch.app.AppSearchResult.RESULT_NOT_FOUND;
 
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.app.VisibilityPermissionDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.PersistType;
+
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * An interface for classes that store and validate document visibility data.
+ * Stores all visibility settings for all databases that AppSearchImpl knows about.
+ * Persists the visibility settings and reloads them on initialization.
+ *
+ * <p>The VisibilityStore creates a {@link VisibilityDocument} for each schema. This document holds
+ * the visibility settings that apply to that schema. The VisibilityStore also creates a
+ * schema for these documents and has its own package and database so that its data doesn't
+ * interfere with any clients' data. It persists the document and schema through AppSearchImpl.
+ *
+ * <p>These visibility settings won't be used in AppSearch Jetpack, we only store them for clients
+ * to look up.
  *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface VisibilityStore {
+public class VisibilityStore {
+    private static final String TAG = "AppSearchVisibilityStor";
     /**
      * 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";
+    public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg";
 
-    @VisibleForTesting
-    String DATABASE_NAME = "VS#Db";
+    static final String VISIBILITY_DATABASE_NAME = "VS#Db";
 
     /**
-     * Sets visibility settings for the given database. Any previous visibility settings will be
-     * overwritten.
+     * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each
+     * schema type.
+     */
+    private final Map<String, VisibilityDocument> mVisibilityDocumentMap = new ArrayMap<>();
+
+    private final AppSearchImpl mAppSearchImpl;
+
+    public VisibilityStore(@NonNull AppSearchImpl appSearchImpl)
+            throws AppSearchException {
+        mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
+
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VISIBILITY_PACKAGE_NAME,
+                VISIBILITY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
+        List<VisibilityDocumentV1> visibilityDocumentsV1s = null;
+        switch (getSchemaResponse.getVersion()) {
+            case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:
+                // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using
+                //  GenericDocument.
+                List<GenericDocument> visibilityDocumentsV0s =
+                        VisibilityStoreMigrationHelperFromV0.getVisibilityDocumentsInVersion0(
+                                getSchemaResponse, mAppSearchImpl);
+                visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0
+                        .toVisibilityDocumentV1(visibilityDocumentsV0s);
+                // fall through
+            case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA:
+                if (visibilityDocumentsV1s == null) {
+                    // We need to read VisibilityDocument in Version 1 from AppSearch instead of
+                    // taking from the above step.
+                    visibilityDocumentsV1s =
+                            VisibilityStoreMigrationHelperFromV1.getVisibilityDocumentsInVersion1(
+                                    mAppSearchImpl);
+                }
+                setLatestSchemaAndDocuments(VisibilityStoreMigrationHelperFromV1
+                        .toVisibilityDocumentsV2(visibilityDocumentsV1s));
+                break;
+            case VisibilityDocument.SCHEMA_VERSION_LATEST:
+                Set<AppSearchSchema> existingVisibilitySchema = getSchemaResponse.getSchemas();
+                if (existingVisibilitySchema.contains(VisibilityDocument.SCHEMA)
+                        && existingVisibilitySchema.contains(VisibilityPermissionDocument.SCHEMA)) {
+                    // The latest Visibility schema is in AppSearch, we must find our schema type.
+                    // Extract all stored Visibility Document into mVisibilityDocumentMap.
+                    loadVisibilityDocumentMap();
+                } else {
+                    // We must have a broken schema. Reset it to the latest version.
+                    // Do NOT set forceOverride to be true here. If you hit problem here it means
+                    // you made a incompatible change in Visibility Schema without update the
+                    // version number. You should bump the version number and create a
+                    // VisibilityStoreMigrationHelper which can analyse the different between the
+                    // old version and the new version to migration user's visibility settings.
+                    mAppSearchImpl.setSchema(
+                            VISIBILITY_PACKAGE_NAME,
+                            VISIBILITY_DATABASE_NAME,
+                            Arrays.asList(VisibilityDocument.SCHEMA,
+                                    VisibilityPermissionDocument.SCHEMA),
+                            /*visibilityDocuments=*/ Collections.emptyList(),
+                            /*forceOverride=*/ false,
+                            /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                            /*setSchemaStatsBuilder=*/ null);
+                }
+                break;
+            default:
+                // We must did something wrong.
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Found unsupported visibility version: " + getSchemaResponse.getVersion());
+        }
+    }
+
+    /**
+     * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous
+     * {@link VisibilityDocument}s with same prefixed schema type 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.
+     * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which
+     *                                    contains schema type's visibility information.
      * @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;
+    public void setVisibility(@NonNull List<VisibilityDocument> prefixedVisibilityDocuments)
+            throws AppSearchException {
+        Preconditions.checkNotNull(prefixedVisibilityDocuments);
+        // Save new setting.
+        for (int i = 0; i < prefixedVisibilityDocuments.size(); i++) {
+            // put VisibilityDocument to AppSearchImpl and mVisibilityDocumentMap. If there is a
+            // VisibilityDocument with same prefixed schema exists, it will be replaced by new
+            // VisibilityDocument in both AppSearch and memory look up map.
+            VisibilityDocument prefixedVisibilityDocument = prefixedVisibilityDocuments.get(i);
+            mAppSearchImpl.putDocument(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                    prefixedVisibilityDocument, /*logger=*/ null);
+            mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),
+                    prefixedVisibilityDocument);
+        }
+        // Now that the visibility document has been written. Persist the newly written data.
+        mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+    }
 
     /**
-     * Checks whether the given package has access to system-surfaceable schemas.
-     *
-     * @param callerUid UID of the app that wants to see the data.
+     * Remove the visibility setting for the given prefixed schema type from both AppSearch and
+     * memory look up map.
      */
-    boolean isSchemaSearchableByCaller(
-            @NonNull String packageName,
-            @NonNull String databaseName,
-            @NonNull String prefixedSchema,
-            int callerUid,
-            boolean callerHasSystemAccess);
+    public void removeVisibility(@NonNull Set<String> prefixedSchemaTypes)
+            throws AppSearchException {
+        for (String prefixedSchemaType : prefixedSchemaTypes) {
+            if (mVisibilityDocumentMap.remove(prefixedSchemaType) == null) {
+                // The deleted schema is not all-default setting, we need to remove its
+                // VisibilityDocument from Icing.
+                try {
+                    mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                            VisibilityDocument.NAMESPACE, prefixedSchemaType,
+                            /*removeStatsBuilder=*/null);
+                } catch (AppSearchException e) {
+                    if (e.getResultCode() == RESULT_NOT_FOUND) {
+                        // We are trying to remove this visibility setting, so it's weird but seems
+                        // to be fine if we cannot find it.
+                        Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType
+                                + " to remove.");
+                        return;
+                    }
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /** Gets the {@link VisibilityDocument} for the given prefixed schema type.     */
+    @Nullable
+    public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) {
+        return mVisibilityDocumentMap.get(prefixedSchemaType);
+    }
+
+    /**
+     * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into
+     * {@link #mVisibilityDocumentMap}.
+     */
+    private void loadVisibilityDocumentMap() throws AppSearchException {
+        // Populate visibility settings set
+        List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes();
+        for (int i = 0; i < cachedSchemaTypes.size(); i++) {
+            String prefixedSchemaType = cachedSchemaTypes.get(i);
+            String packageName = PrefixUtil.getPackageName(prefixedSchemaType);
+            if (packageName.equals(VISIBILITY_PACKAGE_NAME)) {
+                continue; // Our own package. Skip.
+            }
+
+            VisibilityDocument visibilityDocument;
+            try {
+                // Note: We use the other clients' prefixed schema type as ids
+                visibilityDocument = new VisibilityDocument(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityDocument.NAMESPACE,
+                                /*id=*/ prefixedSchemaType,
+                                /*typePropertyPaths=*/ Collections.emptyMap()));
+            } catch (AppSearchException e) {
+                if (e.getResultCode() == RESULT_NOT_FOUND) {
+                    // The schema has all default setting and we won't have a VisibilityDocument for
+                    // it.
+                    continue;
+                }
+                // Otherwise, this is some other error we should pass up.
+                throw e;
+            }
+            mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);
+        }
+    }
+
+    /**
+     * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch.
+     */
+    private void setLatestSchemaAndDocuments(@NonNull List<VisibilityDocument> migratedDocuments)
+            throws AppSearchException {
+        // The latest schema type doesn't exist yet. Add it. Set forceOverride true to
+        // delete old schema.
+        mAppSearchImpl.setSchema(
+                VISIBILITY_PACKAGE_NAME,
+                VISIBILITY_DATABASE_NAME,
+                Arrays.asList(VisibilityDocument.SCHEMA,
+                        VisibilityPermissionDocument.SCHEMA),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                /*setSchemaStatsBuilder=*/ null);
+        for (int i = 0; i < migratedDocuments.size(); i++) {
+            VisibilityDocument migratedDocument = migratedDocuments.get(i);
+            mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument);
+            mAppSearchImpl.putDocument(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                    migratedDocument, /*logger=*/ null);
+        }
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
new file mode 100644
index 0000000..edff437
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The helper class to store Visibility Document information of version 0 and handle the upgrade to
+ * version 1.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityStoreMigrationHelperFromV0 {
+    private VisibilityStoreMigrationHelperFromV0() {}
+    /** Prefix to add to all visibility document ids. IcingSearchEngine doesn't allow empty ids. */
+    private static final String DEPRECATED_ID_PREFIX = "uri:";
+
+    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
+    @VisibleForTesting
+    static final String DEPRECATED_VISIBILITY_SCHEMA_TYPE = "VisibilityType";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    @VisibleForTesting
+    static final String DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY =
+            "notPlatformSurfaceable";
+
+    /** Property that holds nested documents of package accessible schemas. */
+    @VisibleForTesting
+    static final String DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY = "packageAccessible";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    @VisibleForTesting
+    static final String DEPRECATED_PACKAGE_SCHEMA_TYPE = "PackageAccessibleType";
+
+    /** Property that holds the prefixed schema type that is accessible by some package. */
+    @VisibleForTesting
+    static final String DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY = "accessibleSchema";
+
+    /** Property that holds the package name that can access a schema. */
+    @VisibleForTesting
+    static final String DEPRECATED_PACKAGE_NAME_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    @VisibleForTesting
+    static final String DEPRECATED_SHA_256_CERT_PROPERTY = "sha256Cert";
+
+//    The visibility schema of version 0.
+//---------------------------------------------------------------------------------------------
+//    Schema of DEPRECATED_VISIBILITY_SCHEMA_TYPE:
+//    new AppSearchSchema.Builder(
+//            DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+//            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                    DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+//                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+//                    .build())
+//            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+//                    DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY,
+//                    DEPRECATED_PACKAGE_SCHEMA_TYPE)
+//                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+//                    .build())
+//            .build();
+//    Schema of DEPRECATED_PACKAGE_SCHEMA_TYPE:
+//    new AppSearchSchema.Builder(DEPRECATED_PACKAGE_SCHEMA_TYPE)
+//        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                 DEPRECATED_PACKAGE_NAME_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+//                DEPRECATED_SHA_256_CERT_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+//                DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY)
+//                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+//                .build())
+//        .build();
+//---------------------------------------------------------------------------------------------
+
+    /** Returns whether the given schema type is deprecated.     */
+    static boolean isDeprecatedType(@NonNull String schemaType) {
+        return schemaType.equals(DEPRECATED_VISIBILITY_SCHEMA_TYPE)
+                || schemaType.equals(DEPRECATED_PACKAGE_SCHEMA_TYPE);
+    }
+
+    /**
+     * Adds a prefix to create a deprecated visibility document's id.
+     *
+     * @param packageName Package to which the visibility doc refers.
+     * @param databaseName Database to which the visibility doc refers.
+     * @return deprecated visibility document's id.
+     */
+    @NonNull
+    static String getDeprecatedVisibilityDocumentId(
+            @NonNull String packageName, @NonNull String databaseName) {
+        return DEPRECATED_ID_PREFIX + PrefixUtil.createPrefix(packageName, databaseName);
+    }
+
+    /**  Reads all stored deprecated Visibility Document in version 0 from icing. */
+    static List<GenericDocument> getVisibilityDocumentsInVersion0(
+            @NonNull GetSchemaResponse getSchemaResponse,
+            @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
+        if (!hasDeprecatedType(getSchemaResponse)) {
+            return new ArrayList<>();
+        }
+        Map<String, Set<String>> packageToDatabases = appSearchImpl.getPackageToDatabases();
+        List<GenericDocument> deprecatedDocuments = new ArrayList<>(packageToDatabases.size());
+        for (Map.Entry<String, Set<String>> entry : packageToDatabases.entrySet()) {
+            String packageName = entry.getKey();
+            if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
+                continue; // Our own package. Skip.
+            }
+            for (String databaseName : entry.getValue()) {
+                try {
+                    // Note: We use the other clients' prefixed names as ids
+                    deprecatedDocuments.add(appSearchImpl.getDocument(
+                            VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                            VisibilityStore.VISIBILITY_DATABASE_NAME,
+                            VisibilityDocument.NAMESPACE,
+                            getDeprecatedVisibilityDocumentId(packageName, databaseName),
+                            /*typePropertyPaths=*/ Collections.emptyMap()));
+                } 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;
+                }
+            }
+        }
+        return deprecatedDocuments;
+    }
+
+    /**
+     * Converts the given list of deprecated Visibility Documents into a Map of {@code
+     * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
+     *
+     * @param visibilityDocumentV0s          The deprecated Visibility Document we found.
+     */
+    @NonNull
+    static List<VisibilityDocumentV1> toVisibilityDocumentV1(
+            @NonNull List<GenericDocument> visibilityDocumentV0s) {
+        Map<String, VisibilityDocumentV1.Builder> documentBuilderMap = new ArrayMap<>();
+
+        // Set all visibility information into documentBuilderMap
+        for (int i = 0; i < visibilityDocumentV0s.size(); i++) {
+            GenericDocument visibilityDocumentV0 = visibilityDocumentV0s.get(i);
+
+            // Read not displayed by system property field.
+            String[] notDisplayedBySystemSchemas = visibilityDocumentV0.getPropertyStringArray(
+                    DEPRECATED_NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+            if (notDisplayedBySystemSchemas != null) {
+                for (String notDisplayedBySystemSchema : notDisplayedBySystemSchemas) {
+                    // SetSchemaRequest.Builder.build() make sure all schemas that has visibility
+                    // setting must present in the requests.
+                    VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
+                            documentBuilderMap, notDisplayedBySystemSchema);
+                    visibilityBuilder.setNotDisplayedBySystem(true);
+                }
+            }
+
+            // Read visible to packages field.
+            GenericDocument[] deprecatedPackageDocuments = visibilityDocumentV0
+                    .getPropertyDocumentArray(DEPRECATED_VISIBLE_TO_PACKAGES_PROPERTY);
+            if (deprecatedPackageDocuments != null) {
+                for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
+                    String prefixedSchemaType = deprecatedPackageDocument
+                            .getPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY);
+                    VisibilityDocumentV1.Builder visibilityBuilder = getOrCreateBuilder(
+                            documentBuilderMap, prefixedSchemaType);
+                    visibilityBuilder.addVisibleToPackage(new PackageIdentifier(
+                            deprecatedPackageDocument.getPropertyString(
+                                    DEPRECATED_PACKAGE_NAME_PROPERTY),
+                            deprecatedPackageDocument.getPropertyBytes(
+                                    DEPRECATED_SHA_256_CERT_PROPERTY)));
+                }
+            }
+        }
+        List<VisibilityDocumentV1> visibilityDocumentsV1 =
+                new ArrayList<>(documentBuilderMap.size());
+        for (Map.Entry<String, VisibilityDocumentV1.Builder> entry :
+                documentBuilderMap.entrySet()) {
+            visibilityDocumentsV1.add(entry.getValue().build());
+        }
+        return visibilityDocumentsV1;
+    }
+
+    /**
+     * Return whether the database maybe has the oldest version of deprecated schema.
+     *
+     * <p> Since the current version number is 0, it is possible that the database is just empty
+     * and it return 0 as the default version number. So we need to check if the deprecated document
+     * presents to trigger the migration.
+     */
+    private static boolean hasDeprecatedType(@NonNull GetSchemaResponse getSchemaResponse) {
+        for (AppSearchSchema schema : getSchemaResponse.getSchemas()) {
+            if (VisibilityStoreMigrationHelperFromV0
+                    .isDeprecatedType(schema.getSchemaType())) {
+                // Found deprecated type, we need to migrate visibility Document. And it's
+                // not possible for us to find the latest visibility schema.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @NonNull
+    private static VisibilityDocumentV1.Builder getOrCreateBuilder(
+            @NonNull Map<String, VisibilityDocumentV1.Builder> documentBuilderMap,
+            @NonNull String schemaType) {
+        VisibilityDocumentV1.Builder builder = documentBuilderMap.get(schemaType);
+        if (builder == null) {
+            builder = new VisibilityDocumentV1.Builder(/*id=*/ schemaType);
+            documentBuilderMap.put(schemaType, builder);
+        }
+        return builder;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
new file mode 100644
index 0000000..cd3e2c5
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AppSearchResult;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The helper class to store Visibility Document information of version 1 and handle the upgrade to
+ * latest version
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityStoreMigrationHelperFromV1 {
+    private VisibilityStoreMigrationHelperFromV1() {}
+
+    /** Enum in {@link androidx.appsearch.app.SetSchemaRequest} AppSearch supported role. */
+    @VisibleForTesting
+    static final int DEPRECATED_ROLE_HOME = 1;
+
+    /** Enum in {@link androidx.appsearch.app.SetSchemaRequest} AppSearch supported role. */
+    @VisibleForTesting
+    static final int DEPRECATED_ROLE_ASSISTANT = 2;
+
+    /**  Reads all stored deprecated Visibility Document in version 0 from icing. */
+    static List<VisibilityDocumentV1> getVisibilityDocumentsInVersion1(
+            @NonNull AppSearchImpl appSearchImpl) throws AppSearchException {
+        List<String> allPrefixedSchemaTypes = appSearchImpl.getAllPrefixedSchemaTypes();
+        List<VisibilityDocumentV1> visibilityDocumentV1s =
+                new ArrayList<>(allPrefixedSchemaTypes.size());
+        for (int i = 0; i < allPrefixedSchemaTypes.size(); i++) {
+            String packageName = PrefixUtil.getPackageName(allPrefixedSchemaTypes.get(i));
+            if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
+                continue; // Our own package. Skip.
+            }
+            try {
+                // Note: We use the prefixed schema type as ids
+                visibilityDocumentV1s.add(new VisibilityDocumentV1(appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.VISIBILITY_DATABASE_NAME,
+                        VisibilityDocument.NAMESPACE,
+                        allPrefixedSchemaTypes.get(i),
+                        /*typePropertyPaths=*/ Collections.emptyMap())));
+            } 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;
+            }
+        }
+        return visibilityDocumentV1s;
+    }
+
+    /**
+     * Converts the given list of deprecated Visibility Documents into a Map of {@code
+     * <PrefixedSchemaType, VisibilityDocument.Builder of the latest version>}.
+     *
+     * @param visibilityDocumentV1s          The deprecated Visibility Document we found.
+     */
+    @NonNull
+    static List<VisibilityDocument> toVisibilityDocumentsV2(
+            @NonNull List<VisibilityDocumentV1> visibilityDocumentV1s) {
+        List<VisibilityDocument> latestVisibilityDocuments =
+                new ArrayList<>(visibilityDocumentV1s.size());
+        for (int i = 0; i < visibilityDocumentV1s.size(); i++) {
+            VisibilityDocumentV1 visibilityDocumentV1 = visibilityDocumentV1s.get(i);
+            Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
+            Set<Integer> deprecatedVisibleToRoles = visibilityDocumentV1.getVisibleToRoles();
+            if (deprecatedVisibleToRoles != null) {
+                for (int deprecatedVisibleToRole : deprecatedVisibleToRoles) {
+                    Set<Integer> visibleToPermission = new ArraySet<>();
+                    switch (deprecatedVisibleToRole) {
+                        case DEPRECATED_ROLE_HOME:
+                            visibleToPermission.add(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA);
+                            break;
+                        case DEPRECATED_ROLE_ASSISTANT:
+                            visibleToPermission.add(SetSchemaRequest
+                                    .READ_ASSISTANT_APP_SEARCH_DATA);
+                            break;
+                    }
+                    visibleToPermissions.add(visibleToPermission);
+                }
+            }
+            Set<Integer> deprecatedVisibleToPermissions =
+                    visibilityDocumentV1.getVisibleToPermissions();
+            if (deprecatedVisibleToPermissions != null) {
+                visibleToPermissions.add(deprecatedVisibleToPermissions);
+            }
+
+            Set<PackageIdentifier> packageIdentifiers = new ArraySet<>();
+            String[] packageNames = visibilityDocumentV1.getPackageNames();
+            byte[][] sha256Certs = visibilityDocumentV1.getSha256Certs();
+            if (packageNames.length == sha256Certs.length) {
+                for (int j = 0; j < packageNames.length; j++) {
+                    packageIdentifiers.add(new PackageIdentifier(packageNames[j], sha256Certs[j]));
+                }
+            }
+            VisibilityDocument.Builder latestVisibilityDocumentBuilder =
+                    new VisibilityDocument.Builder(visibilityDocumentV1.getId())
+                    .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem())
+                    .addVisibleToPackages(packageIdentifiers);
+            if (!visibleToPermissions.isEmpty()) {
+                latestVisibilityDocumentBuilder.setVisibleToPermissions(visibleToPermissions);
+            }
+            latestVisibilityDocuments.add(latestVisibilityDocumentBuilder.build());
+        }
+        return latestVisibilityDocuments;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
new file mode 100644
index 0000000..ea04968
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityUtil {
+    private VisibilityUtil() {}
+
+    /**
+     * Determines whether the calling package has access to the given prefixed schema type.
+     *
+     * <p>Correctly handles access to own data and the situation that visibilityStore and
+     * visibilityChecker are not configured.
+     *
+     * @param callerAccess      Visibility access info of the calling app
+     * @param targetPackageName The package name of the app that owns the data.
+     * @param prefixedSchema    The prefixed schema type the caller wants to access.
+     * @param visibilityStore   Store for visibility information. If not provided, only
+     *                          access to own data will be allowed.
+     * @param visibilityChecker Checker for visibility access. If not provided, only access to
+     *                          own data will be allowed.
+     * @return Whether access by the caller to this prefixed schema should be allowed.
+     */
+    public static boolean isSchemaSearchableByCaller(
+            @NonNull CallerAccess callerAccess,
+            @NonNull String targetPackageName,
+            @NonNull String prefixedSchema,
+            @Nullable VisibilityStore visibilityStore,
+            @Nullable VisibilityChecker visibilityChecker) {
+        Preconditions.checkNotNull(callerAccess);
+        Preconditions.checkNotNull(targetPackageName);
+        Preconditions.checkNotNull(prefixedSchema);
+
+        if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
+            return true;  // Everyone is always allowed to retrieve their own data.
+        }
+        if (visibilityStore == null || visibilityChecker == null) {
+            return false;  // No visibility is configured at this time; no other access possible.
+        }
+        return visibilityChecker.isSchemaSearchableByCaller(
+                callerAccess,
+                targetPackageName,
+                prefixedSchema,
+                visibilityStore);
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/api/current.txt b/appsearch/appsearch-platform-storage/api/current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/current.txt
+++ b/appsearch/appsearch-platform-storage/api/current.txt
@@ -2,8 +2,10 @@
 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);
+    method @Deprecated 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.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
 
   public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt b/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch-platform-storage/api/public_plus_experimental_current.txt
@@ -2,8 +2,10 @@
 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);
+    method @Deprecated 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.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
 
   public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/api/restricted_current.txt b/appsearch/appsearch-platform-storage/api/restricted_current.txt
index 881789d..0b3b2e6 100644
--- a/appsearch/appsearch-platform-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-platform-storage/api/restricted_current.txt
@@ -2,8 +2,10 @@
 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);
+    method @Deprecated 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.GlobalSearchSession!> createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method @Deprecated public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
 
   public static final class PlatformStorage.GlobalSearchContext {
diff --git a/appsearch/appsearch-platform-storage/build.gradle b/appsearch/appsearch-platform-storage/build.gradle
index 73bb4b7..e4c1f37 100644
--- a/appsearch/appsearch-platform-storage/build.gradle
+++ b/appsearch/appsearch-platform-storage/build.gradle
@@ -25,8 +25,9 @@
     api("androidx.annotation:annotation:1.1.0")
 
     implementation project(":appsearch:appsearch")
+    implementation('androidx.collection:collection:1.2.0')
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
-    implementation("androidx.core:core:1.2.0")
+    implementation("androidx.core:core:1.7.0")
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
diff --git a/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java
new file mode 100644
index 0000000..3ee507c
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverterTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build;
+
+import androidx.appsearch.app.AppSearchResult;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+
+public class AppSearchResultToPlatformConverterTest {
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public void testPlatformAppSearchResultToJetpack_catchException() {
+        android.app.appsearch.AppSearchResult<String> platformResult =
+                android.app.appsearch.AppSearchResult.newSuccessfulResult("42");
+        AppSearchResult<Integer> jetpackResult =
+                AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
+                        platformResult,
+                        platformValue -> {
+                            throw new IllegalArgumentException("Test exception");
+                        }
+                );
+        assertThat(jetpackResult.getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_INVALID_ARGUMENT);
+        assertThat(jetpackResult.getErrorMessage()).contains("Test exception");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public void testPlatformAppSearchResultToFuture_catchException() {
+        android.app.appsearch.AppSearchResult<String> platformResult =
+                android.app.appsearch.AppSearchResult.newSuccessfulResult("42");
+        ResolvableFuture<Integer> future = ResolvableFuture.create();
+        AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                platformResult,
+                future,
+                platformValue -> {
+                    throw new IllegalArgumentException("Test exception");
+                }
+        );
+        ExecutionException e = assertThrows(ExecutionException.class, future::get);
+        assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
+        assertThat(e).hasCauseThat().hasMessageThat().contains("Test exception");
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 34ab614..336b213 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -17,6 +17,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.Features;
+import androidx.core.os.BuildCompat;
 
 /**
  * An implementation of {@link Features}. Feature availability is dependent on Android API
@@ -25,16 +26,23 @@
 final class FeaturesImpl implements Features {
 
     @Override
+    // TODO(b/201316758): Remove once BuildCompat.isAtLeastT is removed
+    @BuildCompat.PrereleaseSdkCheck
     public boolean isFeatureSupported(@NonNull String feature) {
         if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
-            // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is
-            // synced over into service-appsearch.
-            return false;
+            return BuildCompat.isAtLeastT();
         }
-        if (Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER.equals(feature)) {
-            // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is
-            // synced over into service-appsearch.
-            return false;
+        if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
+            return BuildCompat.isAtLeastT();
+        }
+        if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
+            return BuildCompat.isAtLeastT();
+        }
+        if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
+            return BuildCompat.isAtLeastT();
+        }
+        if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
+            return BuildCompat.isAtLeastT();
         }
         return false;
     }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
index 0984e13..6ed63ee 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -15,31 +15,48 @@
  */
 package androidx.appsearch.platformstorage;
 
+import android.annotation.SuppressLint;
 import android.os.Build;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.ObserverSpecToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
+import androidx.collection.ArrayMap;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.Map;
 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)
@@ -49,6 +66,11 @@
     private final Executor mExecutor;
     private final Features mFeatures;
 
+    // Management of observer callbacks.
+    @GuardedBy("mObserverCallbacksLocked")
+    private final Map<ObserverCallback, android.app.appsearch.observer.ObserverCallback>
+            mObserverCallbacksLocked = new ArrayMap<>();
+
     GlobalSearchSessionImpl(
             @NonNull android.app.appsearch.GlobalSearchSession platformSession,
             @NonNull Executor executor,
@@ -58,6 +80,29 @@
         mFeatures = Preconditions.checkNotNull(features);
     }
 
+    @BuildCompat.PrereleaseSdkCheck
+    @NonNull
+    @Override
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+            @NonNull String packageName, @NonNull String databaseName,
+            @NonNull GetByDocumentIdRequest request) {
+        if (!BuildCompat.isAtLeastT()) {
+            throw new UnsupportedOperationException(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
+                    + " is not supported on this AppSearch implementation.");
+        }
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+                ResolvableFuture.create();
+        mPlatformSession.getByDocumentId(packageName, databaseName,
+                RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request),
+                mExecutor,
+                new BatchResultCallbackAdapter<>(
+                        future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
+        return future;
+    }
+
     @Override
     @NonNull
     public SearchResults search(
@@ -74,7 +119,8 @@
 
     @NonNull
     @Override
-    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+    public ListenableFuture<Void> reportSystemUsageAsync(
+            @NonNull ReportSystemUsageRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<Void> future = ResolvableFuture.create();
         mPlatformSession.reportSystemUsage(
@@ -85,34 +131,136 @@
         return future;
     }
 
+    @BuildCompat.PrereleaseSdkCheck
+    @NonNull
+    @Override
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+            @NonNull String databaseName) {
+        // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+        // unsupported build.
+        if (!BuildCompat.isAtLeastT()) {
+            throw new UnsupportedOperationException(
+                    Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
+                            + " is not supported on this AppSearch implementation.");
+        }
+        ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
+        mPlatformSession.getSchema(
+                packageName,
+                databaseName,
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result,
+                        future,
+                        GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
+        return future;
+    }
+
     @NonNull
     @Override
     public Features getFeatures() {
         return mFeatures;
     }
 
+    // TODO(b/193494000): Remove these two lines once BuildCompat.isAtLeastT() is removed.
+    @SuppressLint("NewApi")
+    @BuildCompat.PrereleaseSdkCheck
     @Override
-    public void addObserver(
-            @NonNull String observedPackage,
+    public void registerObserverCallback(
+            @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull AppSearchObserverCallback observer) {
-        Preconditions.checkNotNull(observedPackage);
+            @NonNull ObserverCallback observer) throws AppSearchException {
+        Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(spec);
         Preconditions.checkNotNull(executor);
         Preconditions.checkNotNull(observer);
-        // TODO(b/193494000): Support change notifications in the platform backend once the
-        //  feature is exposed in the Android SDK.
-        throw new UnsupportedOperationException("addObserver not supported for platform yet");
+        // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+        // unsupported build.
+        if (!BuildCompat.isAtLeastT()) {
+            throw new UnsupportedOperationException(
+                    Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
+                            + " is not supported on this AppSearch implementation");
+        }
+
+        synchronized (mObserverCallbacksLocked) {
+            android.app.appsearch.observer.ObserverCallback frameworkCallback =
+                    mObserverCallbacksLocked.get(observer);
+            if (frameworkCallback == null) {
+                // No stub is associated with this package and observer, so we must create one.
+                frameworkCallback = new android.app.appsearch.observer.ObserverCallback() {
+                    @Override
+                    public void onSchemaChanged(
+                            @NonNull android.app.appsearch.observer.SchemaChangeInfo
+                                    platformSchemaChangeInfo) {
+                        SchemaChangeInfo jetpackSchemaChangeInfo =
+                                ObserverSpecToPlatformConverter.toJetpackSchemaChangeInfo(
+                                        platformSchemaChangeInfo);
+                        observer.onSchemaChanged(jetpackSchemaChangeInfo);
+                    }
+
+                    @Override
+                    public void onDocumentChanged(
+                            @NonNull android.app.appsearch.observer.DocumentChangeInfo
+                                    platformDocumentChangeInfo) {
+                        DocumentChangeInfo jetpackDocumentChangeInfo =
+                                ObserverSpecToPlatformConverter.toJetpackDocumentChangeInfo(
+                                        platformDocumentChangeInfo);
+                        observer.onDocumentChanged(jetpackDocumentChangeInfo);
+                    }
+                };
+            }
+
+            // Regardless of whether this stub was fresh or not, we have to register it again
+            // because the user might be supplying a different spec.
+            try {
+                mPlatformSession.registerObserverCallback(
+                        targetPackageName,
+                        ObserverSpecToPlatformConverter.toPlatformObserverSpec(spec),
+                        executor,
+                        frameworkCallback);
+            } catch (android.app.appsearch.exceptions.AppSearchException e) {
+                throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
+            }
+
+            // Now that registration has succeeded, save this stub into our in-memory cache. This
+            // isn't done when errors occur because the user may not call removeObserver if
+            // addObserver threw.
+            mObserverCallbacksLocked.put(observer, frameworkCallback);
+        }
     }
 
+    @SuppressLint("NewApi")
+    @BuildCompat.PrereleaseSdkCheck
     @Override
-    public void removeObserver(
-            @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer) {
-        Preconditions.checkNotNull(observedPackage);
+    public void unregisterObserverCallback(
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+            throws AppSearchException {
+        Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(observer);
-        // TODO(b/193494000): Implement removeObserver
-        throw new UnsupportedOperationException("removeObserver not supported for platform yet");
+        // Superclass is annotated with @RequiresFeature, so we shouldn't get here on an
+        // unsupported build.
+        if (!BuildCompat.isAtLeastT()) {
+            throw new UnsupportedOperationException(
+                    Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK
+                            + " is not supported on this AppSearch implementation");
+        }
+
+        android.app.appsearch.observer.ObserverCallback frameworkCallback;
+        synchronized (mObserverCallbacksLocked) {
+            frameworkCallback = mObserverCallbacksLocked.get(observer);
+            if (frameworkCallback == null) {
+                return;  // No such observer registered. Nothing to do.
+            }
+
+            try {
+                mPlatformSession.unregisterObserverCallback(targetPackageName, frameworkCallback);
+            } catch (android.app.appsearch.exceptions.AppSearchException e) {
+                throw new AppSearchException((int) e.getResultCode(), e.getMessage(), e.getCause());
+            }
+
+            // Only remove from the in-memory map once removal from the service side succeeds
+            mObserverCallbacksLocked.remove(observer);
+        }
     }
 
     @Override
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
index ad345db..f7d8c23 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -209,7 +209,7 @@
      *                {@link AppSearchSession}
      */
     @NonNull
-    public static ListenableFuture<AppSearchSession> createSearchSession(
+    public static ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull SearchContext context) {
         Preconditions.checkNotNull(context);
         AppSearchManager appSearchManager =
@@ -222,7 +222,7 @@
                     if (result.isSuccess()) {
                         future.set(
                                 new SearchSessionImpl(result.getResultValue(), context.mExecutor,
-                                new FeaturesImpl()));
+                                        new FeaturesImpl()));
                     } else {
                         future.setException(
                                 new AppSearchException(
@@ -233,10 +233,23 @@
     }
 
     /**
+     * @deprecated use {@link #createSearchSessionAsync}.
+     *
+     * @param context The {@link SearchContext} contains all information to create a new
+     *                {@link AppSearchSession}
+     */
+    @NonNull
+    @Deprecated
+    public static ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull SearchContext context) {
+        return createSearchSessionAsync(context);
+    }
+
+    /**
      * Opens a new {@link GlobalSearchSession} on this storage.
      */
     @NonNull
-    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync(
             @NonNull GlobalSearchContext context) {
         Preconditions.checkNotNull(context);
         AppSearchManager appSearchManager =
@@ -257,4 +270,14 @@
                 });
         return future;
     }
+
+    /**
+     * @deprecated use {@link #createGlobalSearchSessionAsync}.
+     */
+    @Deprecated
+    @NonNull
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        return createGlobalSearchSessionAsync(context);
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
index f7390c8..85d6567 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -26,6 +26,7 @@
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -58,23 +59,28 @@
 
     @Override
     @NonNull
-    public ListenableFuture<List<SearchResult>> getNextPage() {
+    @BuildCompat.PrereleaseSdkCheck
+    public ListenableFuture<List<SearchResult>> getNextPageAsync() {
         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++) {
-                    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+                    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+                            || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) {
                         // This is a patch for b/197361770, framework-appsearch in Android S will
                         // disable the whole namespace filter if none of given namespaces exist.
-                        // And that will result in Icing return all documents this query is able
-                        // to access.
+                        // And that will result in Icing returns all documents that this query is
+                        // able to access.
                         if (i == 0 && !mSearchSpec.getFilterNamespaces().isEmpty()
                                 && !mSearchSpec.getFilterNamespaces().contains(
                                 frameworkResults.get(i).getGenericDocument().getNamespace())) {
-                            // And in the meantime, since none of the namespace and document that
-                            // use query for exists, we should just return an empty result.
+                            // We should never return a document with a namespace that is not
+                            // required in the request. And also since the bug will only happen
+                            // when the required namespace doesn't exist, we should just return
+                            // an empty result when we found the result contains unexpected
+                            // namespace.
                             future.set(Collections.emptyList());
                             return;
                         }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 4d8f2f2..db93018 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -37,13 +37,14 @@
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
 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.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -75,71 +76,49 @@
 
     @Override
     @NonNull
-    public ListenableFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest request) {
+    @BuildCompat.PrereleaseSdkCheck
+    public ListenableFuture<SetSchemaResponse> setSchemaAsync(@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);
-                    }
-                });
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result,
+                        future,
+                        SetSchemaRequestToPlatformConverter::toJetpackSetSchemaResponse));
         return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<GetSchemaResponse> getSchema() {
+    @BuildCompat.PrereleaseSdkCheck
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
         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);
-                    }
-                });
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result,
+                        future,
+                        GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
         return future;
     }
 
     @NonNull
     @Override
-    public ListenableFuture<Set<String>> getNamespaces() {
+    public ListenableFuture<Set<String>> getNamespacesAsync() {
         ResolvableFuture<Set<String>> future = ResolvableFuture.create();
         mPlatformSession.getNamespaces(
                 mExecutor,
-                result -> {
-                    if (result.isSuccess()) {
-                        future.set(result.getResultValue());
-                    } else {
-                        handleFailedPlatformResult(result, future);
-                    }
-                });
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
         return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
@@ -152,7 +131,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
             @NonNull GetByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
@@ -181,7 +160,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+    public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<Void> future = ResolvableFuture.create();
         mPlatformSession.reportUsage(
@@ -194,7 +173,7 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
             @NonNull RemoveByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
@@ -207,14 +186,14 @@
 
     @Override
     @NonNull
-    public ListenableFuture<Void> remove(
+    @BuildCompat.PrereleaseSdkCheck
+    public ListenableFuture<Void> removeAsync(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
         ResolvableFuture<Void> future = ResolvableFuture.create();
 
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
-                && !searchSpec.getFilterNamespaces().isEmpty()) {
+        if (!BuildCompat.isAtLeastT() && !searchSpec.getFilterNamespaces().isEmpty()) {
             // This is a patch for b/197361770, framework-appsearch in Android S will
             // disable the given namespace filter if it is not empty and none of given namespaces
             // exist.
@@ -223,7 +202,15 @@
             mPlatformSession.getNamespaces(
                     mExecutor,
                     namespaceResult -> {
-                        if (namespaceResult.isSuccess()) {
+                        if (!namespaceResult.isSuccess()) {
+                            future.setException(
+                                    new AppSearchException(
+                                            namespaceResult.getResultCode(),
+                                            namespaceResult.getErrorMessage()));
+                            return;
+                        }
+
+                        try {
                             Set<String> existingNamespaces = namespaceResult.getResultValue();
                             List<String> filterNamespaces = searchSpec.getFilterNamespaces();
                             for (int i = 0; i < filterNamespaces.size(); i++) {
@@ -234,17 +221,18 @@
                                             SearchSpecToPlatformConverter
                                                     .toPlatformSearchSpec(searchSpec),
                                             mExecutor,
-                                            removeResult -> AppSearchResultToPlatformConverter
-                                                    .platformAppSearchResultToFuture(removeResult,
-                                                            future));
+                                            removeResult ->
+                                                    AppSearchResultToPlatformConverter
+                                                            .platformAppSearchResultToFuture(
+                                                                    removeResult, future));
                                     return;
                                 }
                             }
                             // None of the namespace in the given namespace filter exists. Return
                             // early.
                             future.set(null);
-                        } else {
-                            handleFailedPlatformResult(namespaceResult, future);
+                        } catch (Throwable t) {
+                            future.setException(t);
                         }
                     });
         } else {
@@ -261,26 +249,19 @@
 
     @Override
     @NonNull
-    public ListenableFuture<StorageInfo> getStorageInfo() {
+    public ListenableFuture<StorageInfo> getStorageInfoAsync() {
         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);
-                    }
-                });
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future, ResponseToPlatformConverter::toJetpackStorageInfo)
+        );
         return future;
     }
 
     @NonNull
     @Override
-    public ListenableFuture<Void> requestFlush() {
+    public ListenableFuture<Void> requestFlushAsync() {
         ResolvableFuture<Void> future = ResolvableFuture.create();
         // The data in platform will be flushed by scheduled task. This api won't do anything extra
         // flush.
@@ -298,12 +279,4 @@
     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/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
index 1510626..6d9a999 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
@@ -45,11 +45,18 @@
      * {@link androidx.appsearch.app.AppSearchResult}.
      */
     @NonNull
-    public static <T> AppSearchResult<T> platformAppSearchResultToJetpack(
-            @NonNull android.app.appsearch.AppSearchResult<T> platformResult) {
+    public static <PlatformType, JetpackType> AppSearchResult<JetpackType>
+            platformAppSearchResultToJetpack(
+            @NonNull android.app.appsearch.AppSearchResult<PlatformType> platformResult,
+            @NonNull Function<PlatformType, JetpackType> valueMapper) {
         Preconditions.checkNotNull(platformResult);
         if (platformResult.isSuccess()) {
-            return AppSearchResult.newSuccessfulResult(platformResult.getResultValue());
+            try {
+                JetpackType jetpackType = valueMapper.apply(platformResult.getResultValue());
+                return AppSearchResult.newSuccessfulResult(jetpackType);
+            } catch (Throwable t) {
+                return AppSearchResult.throwableToFailedResult(t);
+            }
         }
         return AppSearchResult.newFailedResult(
                 platformResult.getResultCode(), platformResult.getErrorMessage());
@@ -57,20 +64,36 @@
 
     /**
      * Uses the given {@link android.app.appsearch.AppSearchResult} to populate the given
+     * {@link ResolvableFuture}, transforming it using {@code valueMapper}.
+     */
+    public static <PlatformType, JetpackType> void platformAppSearchResultToFuture(
+            @NonNull android.app.appsearch.AppSearchResult<PlatformType> platformResult,
+            @NonNull ResolvableFuture<JetpackType> future,
+            @NonNull Function<PlatformType, JetpackType> valueMapper) {
+        Preconditions.checkNotNull(platformResult);
+        Preconditions.checkNotNull(future);
+        if (platformResult.isSuccess()) {
+            try {
+                JetpackType jetpackType = valueMapper.apply(platformResult.getResultValue());
+                future.set(jetpackType);
+            } catch (Throwable t) {
+                future.setException(t);
+            }
+        } else {
+            future.setException(
+                    new AppSearchException(
+                            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()));
-        }
+        platformAppSearchResultToFuture(platformResult, future, Function.identity());
     }
 
     /**
@@ -89,8 +112,13 @@
         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);
+            try {
+                JetpackValue jetpackValue = valueMapper.apply(success.getValue());
+                jetpackResult.setSuccess(success.getKey(), jetpackValue);
+            } catch (Throwable t) {
+                jetpackResult.setResult(
+                        success.getKey(), AppSearchResult.throwableToFailedResult(t));
+            }
         }
         for (Map.Entry<K, android.app.appsearch.AppSearchResult<PlatformValue>> failure :
                 platformResult.getFailures().entrySet()) {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
index 3838191..004fcc4 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -61,9 +61,24 @@
             } else if (property instanceof boolean[]) {
                 platformBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
             } else if (property instanceof byte[][]) {
-                platformBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+                byte[][] byteValues = (byte[][]) property;
+                // This is a patch for b/204677124, framework-appsearch in Android S and S_V2 will
+                // crash if the user put a document with empty byte[][] or document[].
+                if ((Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+                        || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2)
+                        && byteValues.length == 0) {
+                    continue;
+                }
+                platformBuilder.setPropertyBytes(propertyName, byteValues);
             } else if (property instanceof GenericDocument[]) {
                 GenericDocument[] documentValues = (GenericDocument[]) property;
+                // This is a patch for b/204677124, framework-appsearch in Android S and S_V2 will
+                // crash if the user put a document with empty byte[][] or document[].
+                if ((Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+                        || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2)
+                        && documentValues.length == 0) {
+                    continue;
+                }
                 android.app.appsearch.GenericDocument[] platformSubDocuments =
                         new android.app.appsearch.GenericDocument[documentValues.length];
                 for (int j = 0; j < documentValues.length; j++) {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
new file mode 100644
index 0000000..b69caca
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Preconditions;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link GetSchemaResponse}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class GetSchemaResponseToPlatformConverter {
+    private GetSchemaResponseToPlatformConverter() {}
+
+    /**
+     * Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
+     * {@link GetSchemaResponse}.
+     */
+    @NonNull
+    @BuildCompat.PrereleaseSdkCheck
+    public static GetSchemaResponse toJetpackGetSchemaResponse(
+            @NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
+        Preconditions.checkNotNull(platformResponse);
+        GetSchemaResponse.Builder jetpackBuilder;
+        if (!BuildCompat.isAtLeastT()) {
+            // Android API level in S-v2 and lower won't have any supported feature.
+            jetpackBuilder = new GetSchemaResponse.Builder(/*getVisibilitySettingSupported=*/false);
+        } else {
+            // The regular builder has all supported features.
+            jetpackBuilder = new GetSchemaResponse.Builder();
+        }
+        for (android.app.appsearch.AppSearchSchema platformSchema : platformResponse.getSchemas()) {
+            jetpackBuilder.addSchema(SchemaToPlatformConverter.toJetpackSchema(platformSchema));
+        }
+        jetpackBuilder.setVersion(platformResponse.getVersion());
+        if (BuildCompat.isAtLeastT()) {
+            // Convert schemas not displayed by system
+            for (String schemaTypeNotDisplayedBySystem :
+                    platformResponse.getSchemaTypesNotDisplayedBySystem()) {
+                jetpackBuilder.addSchemaTypeNotDisplayedBySystem(schemaTypeNotDisplayedBySystem);
+            }
+            // Convert schemas visible to packages
+            convertSchemasVisibleToPackages(platformResponse, jetpackBuilder);
+            // Convert schemas visible to permissions
+            for (Map.Entry<String, Set<Set<Integer>>> entry :
+                    platformResponse.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
+                jetpackBuilder.setRequiredPermissionsForSchemaTypeVisibility(entry.getKey(),
+                        entry.getValue());
+            }
+        }
+        return jetpackBuilder.build();
+    }
+
+    /**
+     * Adds package visibilities in a platform {@link android.app.appsearch.GetSchemaResponse} into
+     * the given jetpack {@link GetSchemaResponse}.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private static void convertSchemasVisibleToPackages(
+            @NonNull android.app.appsearch.GetSchemaResponse platformResponse,
+            @NonNull GetSchemaResponse.Builder jetpackBuilder) {
+        // TODO(b/205749173): If there were no packages, getSchemaTypesVisibleToPackages
+        //  incorrectly returns {@code null} in some prerelease versions of Android T. Remove
+        //  this workaround after the issue is fixed in T.
+        Map<String, Set<android.app.appsearch.PackageIdentifier>> schemaTypesVisibleToPackages =
+                platformResponse.getSchemaTypesVisibleToPackages();
+        if (schemaTypesVisibleToPackages != null) {
+            for (Map.Entry<String, Set<android.app.appsearch.PackageIdentifier>> entry
+                    : schemaTypesVisibleToPackages.entrySet()) {
+                Set<PackageIdentifier> jetpackPackageIdentifiers =
+                        new ArraySet<>(entry.getValue().size());
+                for (android.app.appsearch.PackageIdentifier frameworkPackageIdentifier
+                        : entry.getValue()) {
+                    jetpackPackageIdentifiers.add(new PackageIdentifier(
+                            frameworkPackageIdentifier.getPackageName(),
+                            frameworkPackageIdentifier.getSha256Certificate()));
+                }
+                jetpackBuilder.setSchemaTypeVisibleToPackages(
+                        entry.getKey(), jetpackPackageIdentifiers);
+            }
+        }
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java
new file mode 100644
index 0000000..41ca827
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/ObserverSpecToPlatformConverter.java
@@ -0,0 +1,80 @@
+/*
+ * 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.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link ObserverSpec}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public final class ObserverSpecToPlatformConverter {
+    private ObserverSpecToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link ObserverSpec} into a platform
+     * {@link android.app.appsearch.observer.ObserverSpec}.
+     */
+    @NonNull
+    public static android.app.appsearch.observer.ObserverSpec toPlatformObserverSpec(
+            @NonNull ObserverSpec jetpackSpec) {
+        Preconditions.checkNotNull(jetpackSpec);
+        return new android.app.appsearch.observer.ObserverSpec.Builder()
+                .addFilterSchemas(jetpackSpec.getFilterSchemas())
+                .build();
+    }
+
+    /**
+     * Translates a platform {@link androidx.appsearch.observer.SchemaChangeInfo} into a jetpack
+     * {@link SchemaChangeInfo}.
+     */
+    @NonNull
+    public static SchemaChangeInfo toJetpackSchemaChangeInfo(
+            @NonNull android.app.appsearch.observer.SchemaChangeInfo platformInfo) {
+        Preconditions.checkNotNull(platformInfo);
+        return new SchemaChangeInfo(
+                platformInfo.getPackageName(),
+                platformInfo.getDatabaseName(),
+                platformInfo.getChangedSchemaNames());
+    }
+
+    /**
+     * Translates a platform {@link androidx.appsearch.observer.DocumentChangeInfo} into a jetpack
+     * {@link DocumentChangeInfo}.
+     */
+    @NonNull
+    public static DocumentChangeInfo toJetpackDocumentChangeInfo(
+            @NonNull android.app.appsearch.observer.DocumentChangeInfo platformInfo) {
+        Preconditions.checkNotNull(platformInfo);
+        return new DocumentChangeInfo(
+                platformInfo.getPackageName(),
+                platformInfo.getDatabaseName(),
+                platformInfo.getNamespace(),
+                platformInfo.getSchemaName(),
+                platformInfo.getChangedDocumentIds());
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
index 841cbe7..707234d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -23,6 +23,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -37,6 +38,7 @@
     private SearchResultToPlatformConverter() {}
 
     /** Translates from Platform to Jetpack versions of {@link SearchResult}. */
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static SearchResult toJetpackSearchResult(
             @NonNull android.app.appsearch.SearchResult platformResult) {
@@ -56,13 +58,13 @@
         return builder.build();
     }
 
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     private static SearchResult.MatchInfo toJetpackMatchInfo(
             @NonNull android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
         Preconditions.checkNotNull(platformMatchInfo);
-        // TODO(b/201316758) : Copy over submatch range info once it is added to
-        //  framework-appsearch.
-        return new SearchResult.MatchInfo.Builder(platformMatchInfo.getPropertyPath())
+        SearchResult.MatchInfo.Builder builder = new SearchResult.MatchInfo.Builder(
+                platformMatchInfo.getPropertyPath())
                 .setExactMatchRange(
                         new SearchResult.MatchRange(
                                 platformMatchInfo.getExactMatchRange().getStart(),
@@ -70,7 +72,13 @@
                 .setSnippetRange(
                         new SearchResult.MatchRange(
                                 platformMatchInfo.getSnippetRange().getStart(),
-                                platformMatchInfo.getSnippetRange().getEnd()))
-                .build();
+                                platformMatchInfo.getSnippetRange().getEnd()));
+        if (BuildCompat.isAtLeastT()) {
+            builder.setSubmatchRange(
+                    new SearchResult.MatchRange(
+                            platformMatchInfo.getSubmatchRange().getStart(),
+                            platformMatchInfo.getSubmatchRange().getEnd()));
+        }
+        return builder.build();
     }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
index ca52112..6950427 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
@@ -27,10 +27,12 @@
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 /**
  * Translates between Platform and Jetpack versions of {@link SetSchemaRequest}.
@@ -45,6 +47,7 @@
      * Translates a jetpack {@link SetSchemaRequest} into a platform
      * {@link android.app.appsearch.SetSchemaRequest}.
      */
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
             @NonNull SetSchemaRequest jetpackRequest) {
@@ -70,6 +73,20 @@
                                 jetpackPackageIdentifier.getSha256Certificate()));
             }
         }
+        if (!jetpackRequest.getRequiredPermissionsForSchemaTypeVisibility().isEmpty()) {
+            if (!BuildCompat.isAtLeastT()) {
+                throw new UnsupportedOperationException(
+                        "Set required permissions for schema type visibility are not supported "
+                                + "with this backend/Android API level combination.");
+            }
+            for (Map.Entry<String, Set<Set<Integer>>> entry :
+                    jetpackRequest.getRequiredPermissionsForSchemaTypeVisibility().entrySet()) {
+                for (Set<Integer> permissionGroup : entry.getValue()) {
+                    platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(
+                            entry.getKey(), permissionGroup);
+                }
+            }
+        }
         for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
             Migrator jetpackMigrator = entry.getValue();
             android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
@@ -141,7 +158,8 @@
                     migrationFailure.getDocumentId(),
                     migrationFailure.getSchemaType(),
                     AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
-                            migrationFailure.getAppSearchResult())));
+                            migrationFailure.getAppSearchResult(), Function.identity()))
+            );
         }
         return jetpackBuilder.build();
     }
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
index f578822..3c2943e 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
@@ -27,6 +27,7 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,7 +60,7 @@
             @NonNull AppSearchSession session, @NonNull String namespace, @NonNull String... ids)
             throws Exception {
         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
-                session.getByDocumentId(
+                session.getByDocumentIdAsync(
                         new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
         assertThat(result.getSuccesses()).hasSize(ids.length);
         assertThat(result.getFailures()).isEmpty();
@@ -76,7 +77,7 @@
             @NonNull AppSearchSession session, @NonNull GetByDocumentIdRequest request)
             throws Exception {
         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
-                session.getByDocumentId(request));
+                session.getByDocumentIdAsync(request));
         Set<String> ids = request.getIds();
         assertThat(result.getSuccesses()).hasSize(ids.size());
         assertThat(result.getFailures()).isEmpty();
@@ -104,12 +105,24 @@
     @NonNull
     public static List<SearchResult> retrieveAllSearchResults(@NonNull SearchResults searchResults)
             throws Exception {
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         List<SearchResult> results = new ArrayList<>();
         while (!page.isEmpty()) {
             results.addAll(page);
-            page = searchResults.getNextPage().get();
+            page = searchResults.getNextPageAsync().get();
         }
         return results;
     }
+
+    /**
+     * Creates a mock {@link VisibilityChecker}.
+     * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
+     * @return
+     */
+    @NonNull
+    public static VisibilityChecker createMockVisibilityChecker(
+            @NonNull Set<String> visiblePrefixedSchemas) {
+        return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
+                visiblePrefixedSchemas.contains(prefixedSchema);
+    }
 }
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
index 93cb2ce..d1dd006 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/TestObserverCallback.java
@@ -19,15 +19,15 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.observer.AppSearchObserverCallback;
 import androidx.appsearch.observer.DocumentChangeInfo;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.SchemaChangeInfo;
 
 import java.util.ArrayList;
 import java.util.List;
 
 /**
- * An implementation of {@link androidx.appsearch.observer.AppSearchObserverCallback} for testing
+ * An implementation of {@link androidx.appsearch.observer.ObserverCallback} for testing
  * that caches its notifications in memory.
  *
  * <p>You should wait for all notifications to be delivered using {@link #waitForNotificationCount}
@@ -36,7 +36,7 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class TestObserverCallback implements AppSearchObserverCallback {
+public class TestObserverCallback implements ObserverCallback {
     private final Object mLock = new Object();
 
     private final List<SchemaChangeInfo> mSchemaChanges = new ArrayList<>();
@@ -111,6 +111,16 @@
         return mDocumentChanges;
     }
 
+    /** Removes all notifications captured by this callback and resets the count to 0. */
+    public void clear() {
+        synchronized (mLock) {
+            mSchemaChanges.clear();
+            mDocumentChanges.clear();
+            mNotificationCountLocked = 0;
+            mLock.notifyAll();
+        }
+    }
+
     private void incrementNotificationCountLocked() {
         synchronized (mLock) {
             mNotificationCountLocked++;
diff --git a/appsearch/appsearch/api/api_lint.ignore b/appsearch/appsearch/api/api_lint.ignore
index 36bb389..aa14c59 100644
--- a/appsearch/appsearch/api/api_lint.ignore
+++ b/appsearch/appsearch/api/api_lint.ignore
@@ -19,6 +19,8 @@
     Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
 AsyncSuffixFuture: androidx.appsearch.app.AppSearchSession#setSchema(androidx.appsearch.app.SetSchemaRequest):
     Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
+AsyncSuffixFuture: androidx.appsearch.app.GlobalSearchSession#getSchema(String, String):
+    Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
 AsyncSuffixFuture: androidx.appsearch.app.GlobalSearchSession#reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest):
     Methods returning com.google.common.util.concurrent.ListenableFuture should have a suffix *Async to reserve unmodified name for a suspend function
 AsyncSuffixFuture: androidx.appsearch.app.SearchResults#getNextPage():
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -177,18 +177,28 @@
 
   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!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method @Deprecated public default 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<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    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 @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+    method @Deprecated public default 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!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+    method @Deprecated public default 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!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
     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);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
   }
 
   public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
 
   public interface Features {
     method public boolean isFeatureSupported(String);
-    field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+    field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+    field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
   }
 
@@ -266,6 +279,9 @@
   }
 
   public final class GetSchemaResponse {
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     method @IntRange(from=0) public int getVersion();
   }
@@ -273,17 +289,24 @@
   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.Builder addSchemaTypeNotDisplayedBySystem(String);
     method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
   }
 
   public interface GlobalSearchSession extends java.io.Closeable {
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
     method public void close();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public abstract class Migrator {
@@ -395,7 +418,8 @@
 
   public interface SearchResults extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
   }
 
   public final class SearchSpec {
@@ -453,20 +477,31 @@
 
   public final class SetSchemaRequest {
     method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+    method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     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();
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
     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);
@@ -535,19 +570,20 @@
 
 package androidx.appsearch.observer {
 
-  public interface AppSearchObserverCallback {
-    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
-    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
-  }
-
   public final class DocumentChangeInfo {
-    ctor public DocumentChangeInfo(String, String, String, String);
+    ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedDocumentIds();
     method public String getDatabaseName();
     method public String getNamespace();
     method public String getPackageName();
     method public String getSchemaName();
   }
 
+  public interface ObserverCallback {
+    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+  }
+
   public final class ObserverSpec {
     method public java.util.Set<java.lang.String!> getFilterSchemas();
   }
@@ -562,7 +598,8 @@
   }
 
   public final class SchemaChangeInfo {
-    ctor public SchemaChangeInfo(String, String);
+    ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedSchemaNames();
     method public String getDatabaseName();
     method public String getPackageName();
   }
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -177,18 +177,28 @@
 
   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!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method @Deprecated public default 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<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    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 @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+    method @Deprecated public default 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!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+    method @Deprecated public default 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!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
     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);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
   }
 
   public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
 
   public interface Features {
     method public boolean isFeatureSupported(String);
-    field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+    field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+    field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
   }
 
@@ -266,6 +279,9 @@
   }
 
   public final class GetSchemaResponse {
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     method @IntRange(from=0) public int getVersion();
   }
@@ -273,17 +289,24 @@
   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.Builder addSchemaTypeNotDisplayedBySystem(String);
     method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
   }
 
   public interface GlobalSearchSession extends java.io.Closeable {
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
     method public void close();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public abstract class Migrator {
@@ -395,7 +418,8 @@
 
   public interface SearchResults extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
   }
 
   public final class SearchSpec {
@@ -453,20 +477,31 @@
 
   public final class SetSchemaRequest {
     method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+    method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     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();
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
     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);
@@ -535,19 +570,20 @@
 
 package androidx.appsearch.observer {
 
-  public interface AppSearchObserverCallback {
-    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
-    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
-  }
-
   public final class DocumentChangeInfo {
-    ctor public DocumentChangeInfo(String, String, String, String);
+    ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedDocumentIds();
     method public String getDatabaseName();
     method public String getNamespace();
     method public String getPackageName();
     method public String getSchemaName();
   }
 
+  public interface ObserverCallback {
+    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+  }
+
   public final class ObserverSpec {
     method public java.util.Set<java.lang.String!> getFilterSchemas();
   }
@@ -562,7 +598,8 @@
   }
 
   public final class SchemaChangeInfo {
-    ctor public SchemaChangeInfo(String, String);
+    ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedSchemaNames();
     method public String getDatabaseName();
     method public String getPackageName();
   }
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 91b14d5..0529d14 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -177,18 +177,28 @@
 
   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!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method @Deprecated public default 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<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    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 @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespacesAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfoAsync();
+    method @Deprecated public default 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!>!> putAsync(androidx.appsearch.app.PutDocumentsRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeAsync(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeAsync(String, androidx.appsearch.app.SearchSpec);
+    method @Deprecated public default 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!> reportUsageAsync(androidx.appsearch.app.ReportUsageRequest);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> requestFlushAsync();
     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);
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchemaAsync(androidx.appsearch.app.SetSchemaRequest);
   }
 
   public interface DocumentClassFactory<T> {
@@ -200,7 +210,10 @@
 
   public interface Features {
     method public boolean isFeatureSupported(String);
-    field public static final String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+    field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+    field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+    field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
   }
 
@@ -266,6 +279,9 @@
   }
 
   public final class GetSchemaResponse {
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set<java.lang.String!> getSchemaTypesNotDisplayedBySystem();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemaTypesVisibleToPackages();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     method @IntRange(from=0) public int getVersion();
   }
@@ -273,17 +289,24 @@
   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.Builder addSchemaTypeNotDisplayedBySystem(String);
     method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.util.Set<java.lang.Integer!>!>);
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set<androidx.appsearch.app.PackageIdentifier!>);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
   }
 
   public interface GlobalSearchSession extends java.io.Closeable {
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void addObserver(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.AppSearchObserverCallback);
     method public void close();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_BY_ID) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
     method public androidx.appsearch.app.Features getFeatures();
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER) public void removeObserver(String, androidx.appsearch.observer.AppSearchObserverCallback);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method @Deprecated @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public default com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA) public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void registerObserverCallback(String, androidx.appsearch.observer.ObserverSpec, java.util.concurrent.Executor, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsageAsync(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK) public void unregisterObserverCallback(String, androidx.appsearch.observer.ObserverCallback) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public abstract class Migrator {
@@ -395,7 +418,8 @@
 
   public interface SearchResults extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method @Deprecated public default com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPage();
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.appsearch.app.SearchResult!>!> getNextPageAsync();
   }
 
   public final class SearchSpec {
@@ -453,20 +477,31 @@
 
   public final class SetSchemaRequest {
     method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
+    method public java.util.Map<java.lang.String!,java.util.Set<java.util.Set<java.lang.Integer!>!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
     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();
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6; // 0x6
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CALENDAR = 2; // 0x2
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_CONTACTS = 3; // 0x3
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_EXTERNAL_STORAGE = 4; // 0x4
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_HOME_APP_SEARCH_DATA = 5; // 0x5
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public static final int READ_SMS = 1; // 0x1
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class<?>, java.util.Set<java.lang.Integer!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set<java.lang.Integer!>);
     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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
     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);
@@ -535,19 +570,20 @@
 
 package androidx.appsearch.observer {
 
-  public interface AppSearchObserverCallback {
-    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
-    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
-  }
-
   public final class DocumentChangeInfo {
-    ctor public DocumentChangeInfo(String, String, String, String);
+    ctor public DocumentChangeInfo(String, String, String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedDocumentIds();
     method public String getDatabaseName();
     method public String getNamespace();
     method public String getPackageName();
     method public String getSchemaName();
   }
 
+  public interface ObserverCallback {
+    method public void onDocumentChanged(androidx.appsearch.observer.DocumentChangeInfo);
+    method public void onSchemaChanged(androidx.appsearch.observer.SchemaChangeInfo);
+  }
+
   public final class ObserverSpec {
     method public java.util.Set<java.lang.String!> getFilterSchemas();
   }
@@ -562,7 +598,8 @@
   }
 
   public final class SchemaChangeInfo {
-    ctor public SchemaChangeInfo(String, String);
+    ctor public SchemaChangeInfo(String, String, java.util.Set<java.lang.String!>);
+    method public java.util.Set<java.lang.String!> getChangedSchemaNames();
     method public String getDatabaseName();
     method public String getPackageName();
   }
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index c536467..02e32f9 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -32,9 +32,9 @@
     api('androidx.annotation:annotation:1.1.0')
     api(libs.guavaListenableFuture)
 
+    implementation('androidx.collection:collection:1.2.0')
     implementation('androidx.concurrent:concurrent-futures:1.0.0')
-    implementation('androidx.core:core:1.2.0')
-    implementation('androidx.collection:collection:1.0.0')
+    implementation('androidx.core:core:1.7.0')
 
     androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
     androidTestImplementation project(':appsearch:appsearch-local-storage')
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 b22f8d8..6cf35bb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
@@ -26,9 +26,9 @@
 
 public class AnnotationProcessorLocalTest extends AnnotationProcessorTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 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
index 5dbb8a2..e53c6aa 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
@@ -29,9 +29,9 @@
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AnnotationProcessorPlatformTest extends AnnotationProcessorTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createSearchSession(
+        return PlatformStorage.createSearchSessionAsync(
                 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 684e51f..82c0ae8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -41,12 +41,12 @@
     private AppSearchSession mSession;
     private static final String DB_NAME_1 = "";
 
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+    protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName);
 
     @Before
     public void setUp() throws Exception {
-        mSession = createSearchSession(DB_NAME_1).get();
+        mSession = createSearchSessionAsync(DB_NAME_1).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.
@@ -60,7 +60,8 @@
     }
 
     private void cleanup() throws Exception {
-        mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).build()).get();
     }
 
     @Document
@@ -297,7 +298,7 @@
     public void testAnnotationProcessor() throws Exception {
         //TODO(b/156296904) add test for int, float, GenericDocument, and class with
         // @Document annotation
-        mSession.setSchema(
+        mSession.setSchemaAsync(
                 new SetSchemaRequest.Builder().addDocumentClasses(Card.class, Gift.class).build())
                 .get();
 
@@ -305,7 +306,7 @@
         Gift inputDocument = Gift.createPopulatedGift();
 
         // Index the Gift document and query it.
-        checkIsBatchResultSuccess(mSession.put(
+        checkIsBatchResultSuccess(mSession.putAsync(
                 new PutDocumentsRequest.Builder().addDocuments(inputDocument).build()));
         SearchResults searchResults = mSession.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
@@ -320,7 +321,7 @@
 
     @Test
     public void testAnnotationProcessor_queryByType() throws Exception {
-        mSession.setSchema(
+        mSession.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addDocumentClasses(Card.class, Gift.class)
                         .addSchemas(AppSearchEmail.SCHEMA).build())
@@ -340,7 +341,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mSession.put(
+        checkIsBatchResultSuccess(mSession.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addDocuments(inputDocument1, inputDocument2)
                         .addGenericDocuments(email1).build()));
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
similarity index 93%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
index 125e55f..12393de 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultInternalTest.java
@@ -22,7 +22,8 @@
 
 import org.junit.Test;
 
-public class AppSearchResultTest {
+/** Tests for private APIs of {@link AppSearchResult}. */
+public class AppSearchResultInternalTest {
     @Test
     public void testMapNullPointerException() {
         NullPointerException e = assertThrows(NullPointerException.class, () -> {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
similarity index 95%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index c15f290..4d5b6c6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -23,7 +23,8 @@
 
 import org.junit.Test;
 
-public class GenericDocumentTest {
+/** Tests for private APIs of {@link GenericDocument}. */
+public class GenericDocumentInternalTest {
     @Test
     public void testRecreateFromParcel() {
         GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java
new file mode 100644
index 0000000..f764313
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GetSchemaResponseInternalTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+/** Tests for private APIs of {@link GetSchemaResponse}. */
+public class GetSchemaResponseInternalTest {
+    // TODO(b/205749173): Expose this API and move this test to CTS. Without this API, clients can't
+    //   write unit tests that check their code in an environment where visibility settings are not
+    //   supported.
+    @Test
+    public void testRebuild_noSupportedException() {
+        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();
+
+        GetSchemaResponse.Builder builder =
+                new GetSchemaResponse.Builder(/*getVisibilitySettingSupported=*/false)
+                        .setVersion(42).addSchema(schema1);
+
+        GetSchemaResponse original = builder.build();
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> original.getSchemaTypesNotDisplayedBySystem());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> original.getSchemaTypesVisibleToPackages());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> original.getRequiredPermissionsForSchemaTypeVisibility());
+
+        // rebuild will throw same exception
+        GetSchemaResponse rebuild = builder.setVersion(42).build();
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> rebuild.getSchemaTypesNotDisplayedBySystem());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> rebuild.getSchemaTypesVisibleToPackages());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> original.getRequiredPermissionsForSchemaTypeVisibility());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
similarity index 96%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index 19ccc63..42de090 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -22,7 +22,8 @@
 
 import org.junit.Test;
 
-public class SearchSpecTest {
+/** Tests for private APIs of {@link SearchSpec}. */
+public class SearchSpecInternalTest {
 
     @Test
     public void testGetBundle() {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
similarity index 96%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
index 8aa6148..799584a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
@@ -20,7 +20,8 @@
 
 import org.junit.Test;
 
-public class SetSchemaResponseTest {
+/** Tests for private APIs of {@link SetSchemaResponse}. */
+public class SetSchemaResponseInternalTest {
     @Test
     public void testRebuild() {
         SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
index 91dacdb..80094af 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchResultCtsTest.java
@@ -23,6 +23,14 @@
 import org.junit.Test;
 
 public class AppSearchResultCtsTest {
+    @Test
+    public void testNewSuccessfulResult() {
+        AppSearchResult<String> result = AppSearchResult.newSuccessfulResult("String");
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        assertThat(result.getResultValue()).isEqualTo("String");
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getErrorMessage()).isNull();
+    }
 
     @Test
     public void testResultEquals_identical() {
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
index a980a60..59c17f5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
@@ -118,12 +118,12 @@
 
     private AppSearchSession mDb;
 
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+    protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName);
 
     @Before
     public void setUp() throws Exception {
-        mDb = createSearchSession(DB_NAME).get();
+        mDb = createSearchSessionAsync(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.
@@ -135,13 +135,13 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(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(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
         assertThat(result.getSuccesses()).containsExactly("id0", null);
         assertThat(result.getFailures()).isEmpty();
@@ -150,7 +150,7 @@
     @After
     public void tearDown() throws Exception {
         // Cleanup whatever documents may still exist in these databases.
-        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
     @Test
@@ -165,7 +165,7 @@
                         .build())
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
@@ -185,7 +185,7 @@
                         .build())
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
@@ -198,7 +198,7 @@
         AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
@@ -212,7 +212,7 @@
         AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
@@ -226,7 +226,7 @@
         AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
@@ -239,7 +239,7 @@
         AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
@@ -258,7 +258,7 @@
                         .build())
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
@@ -277,7 +277,7 @@
                         .build())
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
@@ -290,7 +290,7 @@
         AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
                 .build();
 
-        mDb.setSchema(
+        mDb.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
@@ -304,7 +304,7 @@
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(
+                () -> mDb.setSchemaAsync(
                         new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .setVersion(2)     // upgrade version
@@ -319,7 +319,7 @@
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(
+                () -> mDb.setSchemaAsync(
                         new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .build()).get());
@@ -342,7 +342,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(schema).setForceOverride(true).build()).get();
 
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -354,7 +354,7 @@
                 .setPropertyString("To", "testTo example2")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null);
         assertThat(result.getFailures()).isEmpty();
@@ -406,13 +406,13 @@
         };
 
         SetSchemaResponse setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                mDb.setSchemaAsync(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(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
 
         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
         assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -458,7 +458,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
 
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -466,7 +466,7 @@
                 .setPropertyString("To", "testTo example1")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -508,13 +508,13 @@
         };
 
         SetSchemaResponse setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                mDb.setSchemaAsync(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(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
 
         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
         assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -546,7 +546,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
 
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -554,7 +554,7 @@
                 .setPropertyString("To", "testTo example1")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -594,7 +594,7 @@
 
         // SetSchema with forceOverride=false
         ExecutionException exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
                         .setMigrator("testSchema", migrator)
                         .setVersion(3)     // same version
                         .build()).get());
@@ -602,12 +602,12 @@
 
         // SetSchema with forceOverride=true
         SetSchemaResponse setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
                         .setMigrator("testSchema", migrator)
                         .setVersion(3)     // same version
                         .setForceOverride(true).build()).get();
 
-        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+        assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema);
 
         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
         assertThat(setSchemaResponse.getIncompatibleTypes())
@@ -632,7 +632,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(schema).setForceOverride(true).setVersion(2).build()).get();
 
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
@@ -640,7 +640,7 @@
                 .setPropertyString("To", "testTo example1")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -680,7 +680,7 @@
 
         // SetSchema with forceOverride=false
         ExecutionException exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
                         .setMigrator("testSchema", migrator)
                         .setVersion(4)     // upgrade version
                         .build()).get());
@@ -692,14 +692,14 @@
         // set the source schema to AppSearch
         AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema")
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(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(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -730,7 +730,7 @@
         // SetSchema with forceOverride=false
         // Source type exist, destination type doesn't exist.
         ExecutionException exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("sourceSchema", migrator_sourceToNowhere)
                         .setVersion(2).build())   // upgrade version
@@ -742,7 +742,7 @@
         // SetSchema with forceOverride=true
         // Source type exist, destination type doesn't exist.
         exception = assertThrows(ExecutionException.class,
-                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("sourceSchema", migrator_sourceToNowhere)
                         .setForceOverride(true)
@@ -758,7 +758,7 @@
         // set the destination schema to AppSearch
         AppSearchSchema destinationSchema =
                 new AppSearchSchema.Builder("destinationSchema").build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(destinationSchema).setForceOverride(true).build()).get();
 
         Migrator migrator_nowhereToDestination = new Migrator() {
@@ -787,7 +787,7 @@
         // 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)
+                mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("nonExistSchema", migrator_nowhereToDestination)
                         .setVersion(2) //  upgrade version
@@ -796,7 +796,7 @@
 
         // SetSchema with forceOverride=true
         setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+                mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("nonExistSchema", migrator_nowhereToDestination)
                         .setVersion(2) //  upgrade version
@@ -807,7 +807,7 @@
     @Test
     public void testSchemaMigration_nowhereToNowhere() throws Exception {
         // set empty schema
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .setForceOverride(true).build()).get();
         Migrator migrator_nowhereToNowhere = new Migrator() {
             @Override
@@ -835,7 +835,7 @@
         // no matter force override or not, the migrator won't be invoked
         // SetSchema with forceOverride=false
         SetSchemaResponse setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder()
+                mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
                         .setVersion(2)  //  upgrade version
@@ -844,7 +844,7 @@
 
         // SetSchema with forceOverride=true
         setSchemaResponse =
-                mDb.setSchema(new SetSchemaRequest.Builder()
+                mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
                         .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
                         .setVersion(2) //  upgrade version
@@ -857,13 +857,13 @@
         // set the source schema to AppSearch
         AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema")
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(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(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -897,7 +897,7 @@
         };
 
         // SetSchema with forceOverride=false and increase overall version
-        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(destinationSchema)
                 .setMigrator("sourceSchema", migrator)
                 .setForceOverride(false)
@@ -925,7 +925,7 @@
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(sourceSchema).setForceOverride(true).build()).get();
 
         // save a child and an adult to the Person type
@@ -935,7 +935,7 @@
         GenericDocument adultDoc = new GenericDocument.Builder<>(
                 "namespace", "Person2", "Person")
                 .setPropertyLong("Age", 36).build();
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(childDoc, adultDoc).build()));
         assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
         assertThat(result.getFailures()).isEmpty();
@@ -987,7 +987,7 @@
                 .build();
 
         // SetSchema with forceOverride=false and increase overall version
-        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(adultSchema, childSchema)
                 .setMigrator("Person", migrator)
                 .setForceOverride(false)
@@ -1031,7 +1031,7 @@
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(sourceSchemaA, sourceSchemaB).setForceOverride(true).build()).get();
 
         // save 100 docs to each type
@@ -1047,7 +1047,7 @@
                     .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
             putRequestBuilder.addGenericDocuments(docInA, docInB);
         }
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 putRequestBuilder.build()));
         assertThat(result.getFailures()).isEmpty();
 
@@ -1141,7 +1141,7 @@
         };
 
         // SetSchema with forceOverride=false and increase overall version
-        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(destinationSchemaB, destinationSchemaC, destinationSchemaD)
                 .setMigrator("schemaA", migratorA)
                 .setMigrator("schemaB", migratorB)
@@ -1280,7 +1280,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(typeA).setForceOverride(true).setVersion(1).build()).get();
 
         // save a doc to version 1.
@@ -1288,13 +1288,13 @@
                 "namespace", "id", "TypeA")
                 .setPropertyString("subject", "subject")
                 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 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();
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
         assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
         assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
         assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
@@ -1329,7 +1329,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(typeA).setForceOverride(true).setVersion(2).build()).get();
 
         // save a doc to version 2.
@@ -1338,13 +1338,13 @@
                 .setPropertyString("subject", "subject")
                 .setPropertyString("body", "bodyFromA")
                 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 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();
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
         assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
         assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
         assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
@@ -1375,7 +1375,7 @@
                         .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb.setSchema(new SetSchemaRequest.Builder()
+        mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(typeA).setForceOverride(true).setVersion(3).build()).get();
 
         // save a doc to version 2.
@@ -1384,13 +1384,13 @@
                 .setPropertyString("subject", "subject")
                 .setPropertyString("body", "bodyFromB")
                 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.putAsync(
                 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();
+        SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get();
         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
         assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB");
         assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB");
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
index 6525ddd..b6821a0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
@@ -27,9 +27,9 @@
 
 public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 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
index 405d82e..f2b0e7a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationPlatformCtsTest.java
@@ -30,9 +30,9 @@
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AppSearchSchemaMigrationPlatformCtsTest extends AppSearchSchemaMigrationCtsTestBase{
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createSearchSession(
+        return PlatformStorage.createSearchSessionAsync(
                 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
index 9d817be..8f535d6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -26,6 +26,8 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
 
@@ -40,6 +42,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportUsageRequest;
@@ -54,6 +57,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -62,6 +66,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -78,16 +83,16 @@
     private AppSearchSession mDb1;
     private AppSearchSession mDb2;
 
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+    protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName);
 
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+    protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName, @NonNull ExecutorService executor);
 
     @Before
     public void setUp() throws Exception {
-        mDb1 = createSearchSession(DB_NAME_1).get();
-        mDb2 = createSearchSession(DB_NAME_2).get();
+        mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+        mDb2 = createSearchSessionAsync(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.
@@ -101,9 +106,9 @@
     }
 
     private void cleanup() throws Exception {
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
@@ -121,19 +126,19 @@
                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
     }
 
     @Test
     public void testSetSchema_Failure() throws Exception {
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 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()
+                () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(emailSchema1).build()).get()).getCause();
         assertThat(throwable).isInstanceOf(AppSearchException.class);
         AppSearchException exception = (AppSearchException) throwable;
@@ -142,7 +147,7 @@
         assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
 
         throwable = assertThrows(ExecutionException.class,
-                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+                () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
 
         assertThat(throwable).isInstanceOf(AppSearchException.class);
         exception = (AppSearchException) throwable;
@@ -166,17 +171,17 @@
                         .build()
                 ).build();
 
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
                 .setVersion(1).build()).get();
 
-        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchema().get().getSchemas();
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actualSchemaTypes).containsExactly(schema);
 
         // increase version number
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
                 .setVersion(2).build()).get();
 
-        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
         assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
     }
@@ -197,18 +202,18 @@
                 ).build();
 
         // set different version number to different database.
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
                 .setVersion(135).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema)
                 .setVersion(246).build()).get();
 
 
         // check the version has been set correctly.
-        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
         assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
 
-        getSchemaResponse = mDb2.getSchema().get();
+        getSchemaResponse = mDb2.getSchemaAsync().get();
         assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
         assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
     }
@@ -217,7 +222,7 @@
 
     @Test
     public void testSetSchema_addDocumentClasses() throws Exception {
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addDocumentClasses(EmailDocument.class).build()).get();
     }
 // @exportToFramework:endStrip()
@@ -256,13 +261,13 @@
         SetSchemaRequest request2 = new SetSchemaRequest.Builder()
                 .addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build();
 
-        mDb1.setSchema(request1).get();
-        mDb2.setSchema(request2).get();
+        mDb1.setSchemaAsync(request1).get();
+        mDb2.setSchemaAsync(request2).get();
 
-        Set<AppSearchSchema> actual1 = mDb1.getSchema().get().getSchemas();
+        Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actual1).hasSize(2);
         assertThat(actual1).isEqualTo(request1.getSchemas());
-        Set<AppSearchSchema> actual2 = mDb2.getSchema().get().getSchemas();
+        Set<AppSearchSchema> actual2 = mDb2.getSchemaAsync().get().getSchemas();
         assertThat(actual2).hasSize(2);
         assertThat(actual2).isEqualTo(request2.getSchemas());
     }
@@ -296,9 +301,9 @@
                 .build();
 
         // Add it to AppSearch and then obtain it again
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(inSchema, AppSearchEmail.SCHEMA).build()).get();
-        GetSchemaResponse response = mDb1.getSchema().get();
+        GetSchemaResponse response = mDb1.getSchemaAsync().get();
         List<AppSearchSchema> schemas = new ArrayList<>(response.getSchemas());
         assertThat(schemas).containsExactly(inSchema, AppSearchEmail.SCHEMA);
         AppSearchSchema outSchema;
@@ -351,87 +356,213 @@
     }
 
     @Test
+    public void testGetSchema_visibilitySetting() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+        AppSearchSchema emailSchema = 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();
+
+        byte[] shar256Cert1 = new byte[32];
+        Arrays.fill(shar256Cert1, (byte) 1);
+        byte[] shar256Cert2 = new byte[32];
+        Arrays.fill(shar256Cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 =
+                new PackageIdentifier("pkgFoo", shar256Cert1);
+        PackageIdentifier packageIdentifier2 =
+                new PackageIdentifier("pkgBar", shar256Cert2);
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(emailSchema)
+                .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+                .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+                        packageIdentifier1)
+                .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+                        packageIdentifier2)
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+        assertThat(actual).hasSize(1);
+        assertThat(actual).isEqualTo(request.getSchemas());
+        assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly("Email1");
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
+                .containsExactly("Email1", ImmutableSet.of(
+                        packageIdentifier1, packageIdentifier2));
+        assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly("Email1", ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+    }
+
+    @Test
+    public void testGetSchema_visibilitySetting_notSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+        AppSearchSchema emailSchema = 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();
+
+        byte[] shar256Cert1 = new byte[32];
+        Arrays.fill(shar256Cert1, (byte) 1);
+        byte[] shar256Cert2 = new byte[32];
+        Arrays.fill(shar256Cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 =
+                new PackageIdentifier("pkgFoo", shar256Cert1);
+        PackageIdentifier packageIdentifier2 =
+                new PackageIdentifier("pkgBar", shar256Cert2);
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(emailSchema)
+                .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+                .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+                        packageIdentifier1)
+                .setSchemaTypeVisibilityForPackage("Email1", /*visible=*/true,
+                        packageIdentifier2)
+                .build();
+
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+        assertThat(actual).hasSize(1);
+        assertThat(actual).isEqualTo(request.getSchemas());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> getSchemaResponse.getSchemaTypesNotDisplayedBySystem());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> getSchemaResponse.getSchemaTypesVisibleToPackages());
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility());
+    }
+
+    @Test
+    public void testSetSchema_visibilitySettingPermission_notSupported() {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email1").build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(emailSchema)
+                .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS))
+                .build();
+
+        assertThrows(UnsupportedOperationException.class, () ->
+                mDb1.setSchemaAsync(request).get());
+    }
+
+    @Test
     public void testGetNamespaces() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        assertThat(mDb1.getNamespaces().get()).isEmpty();
+        assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
 
         // Index a document
-        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
                 .addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build())
                 .build()));
-        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1");
+        assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1");
 
         // Index additional data
-        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb1.putAsync(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(
+        assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
                 "namespace1", "namespace2", "namespace3");
 
         // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
         checkIsBatchResultSuccess(
-                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
                         "id2").build()));
-        assertThat(mDb1.getNamespaces().get()).containsExactly(
+        assertThat(mDb1.getNamespacesAsync().get()).containsExactly(
                 "namespace1", "namespace2", "namespace3");
 
         // Remove namespace2/id1 -- namespace2 should now be gone
         checkIsBatchResultSuccess(
-                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
                         "id1").build()));
-        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+        assertThat(mDb1.getNamespacesAsync().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");
+        mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+        assertThat(mDb1.getNamespacesAsync().get()).containsExactly("namespace1", "namespace3");
     }
 
     @Test
     public void testGetNamespaces_dbIsolation() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        assertThat(mDb1.getNamespaces().get()).isEmpty();
-        assertThat(mDb2.getNamespaces().get()).isEmpty();
+        assertThat(mDb1.getNamespacesAsync().get()).isEmpty();
+        assertThat(mDb2.getNamespacesAsync().get()).isEmpty();
 
         // Index documents
-        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
                 .addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build())
                 .build()));
-        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
                 .addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build())
                 .build()));
-        checkIsBatchResultSuccess(mDb2.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb2.putAsync(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");
+        assertThat(mDb1.getNamespacesAsync().get())
+                .containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespacesAsync().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");
+        mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+        assertThat(mDb1.getNamespacesAsync().get())
+                .containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespacesAsync().get()).containsExactly("namespace_db2");
     }
 
     @Test
     public void testGetSchema_emptyDB() throws Exception {
-        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
         assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
     }
 
     @Test
     public void testPutDocuments() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -442,18 +573,122 @@
                 .setBody("This is the body of the testPut email")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
     }
 
+    @Test
+    public void testPutDocuments_emptyProperties() throws Exception {
+        // Schema registration. Due to b/204677124 is fixed in Android T. We have different
+        // behaviour when set empty array to bytes and documents between local and platform storage.
+        // This test only test String, long, boolean and double, for byte array and Document will be
+        // test in backend's specific test.
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new StringPropertyConfig.Builder("string")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyBoolean("boolean")
+                .setPropertyString("string")
+                .setPropertyDouble("double")
+                .setPropertyLong("long")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1")
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+        assertThat(outDocuments).hasSize(1);
+        GenericDocument outDocument = outDocuments.get(0);
+        assertThat(outDocument.getPropertyBooleanArray("boolean")).isEmpty();
+        assertThat(outDocument.getPropertyStringArray("string")).isEmpty();
+        assertThat(outDocument.getPropertyDoubleArray("double")).isEmpty();
+        assertThat(outDocument.getPropertyLongArray("long")).isEmpty();
+    }
+
+    @Test
+    public void testPutLargeDocumentBatch() throws Exception {
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
+                new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Creates a large batch of Documents, since we have max document size in Framework which is
+        // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
+        char[] chars = new char[1024];  // 1KiB
+        Arrays.fill(chars, ' ');
+        String body = String.valueOf(chars) + "the end.";
+        List<GenericDocument> inDocuments = new ArrayList<>();
+        GetByDocumentIdRequest.Builder getByDocumentIdRequestBuilder =
+                new GetByDocumentIdRequest.Builder("namespace");
+        for (int i = 0; i < 4000; i++) {
+            GenericDocument inDocument = new GenericDocument.Builder<>(
+                    "namespace", "id" + i, "Type")
+                    .setPropertyString("body", body)
+                    .build();
+            inDocuments.add(inDocument);
+            getByDocumentIdRequestBuilder.addIds("id" + i);
+        }
+
+        // Index documents.
+        AppSearchBatchResult<String, Void> result =
+                mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(inDocuments)
+                        .build()).get();
+        assertThat(result.isSuccess()).isTrue();
+
+        // Query those documents and verify they are same with the input. This also verify
+        // AppSearchResult could handle large batch.
+        SearchResults searchResults = mDb1.search("end", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> outDocuments = convertSearchResultsToDocuments(searchResults);
+        assertThat(inDocuments).containsExactlyElementsIn(outDocuments);
+
+        // Get by document ID and verify they are same with the input. This also verify
+        // AppSearchBatchResult could handle large batch.
+        AppSearchBatchResult<String, GenericDocument> batchResult = mDb1.getByDocumentIdAsync(
+                getByDocumentIdRequestBuilder.build()).get();
+        assertThat(batchResult.isSuccess()).isTrue();
+        for (int i = 0; i < inDocuments.size(); i++) {
+            GenericDocument inDocument = inDocuments.get(i);
+            assertThat(batchResult.getSuccesses().get(inDocument.getId())).isEqualTo(inDocument);
+        }
+    }
+
 // @exportToFramework:startStrip()
 
     @Test
     public void testPut_addDocumentClasses() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addDocumentClasses(EmailDocument.class).build()).get();
 
         // Index a document
@@ -463,7 +698,7 @@
         email.subject = "testPut example";
         email.body = "This is the body of the testPut email";
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addDocuments(email).build()));
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
@@ -497,7 +732,7 @@
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                         .build())
                 .build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
 
         // Try to index a gift. This should fail as it's not in the schema.
@@ -505,19 +740,19 @@
                 new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price",
                         5).build();
         AppSearchBatchResult<String, Void> result =
-                mDb1.put(
+                mDb1.putAsync(
                         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()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(newEmailSchema, giftSchema).build()).get();
 
         // Try to index the document again, which should now work
         checkIsBatchResultSuccess(
-                mDb1.put(
+                mDb1.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
 
         // Indexing an email with a body should also work
@@ -526,7 +761,7 @@
                 .setBody("This is the body of the testPut email")
                 .build();
         checkIsBatchResultSuccess(
-                mDb1.put(
+                mDb1.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
     }
 
@@ -540,14 +775,14 @@
                         .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+        mDb1.setSchemaAsync(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(
+                mDb1.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
         List<GenericDocument> outDocuments =
                 doGet(mDb1, "namespace", "email1");
@@ -558,7 +793,7 @@
         // 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();
+                () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
         assertThat(failResult1).isInstanceOf(AppSearchException.class);
         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
         assertThat(failResult1).hasMessageThat().contains(
@@ -566,10 +801,10 @@
 
         // 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();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
 
         // Make sure the indexed email is gone.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace")
                         .addIds("email1")
                         .build()).get();
@@ -581,7 +816,7 @@
         AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
                 .setSubject("testPut example")
                 .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email2").getErrorMessage())
@@ -599,15 +834,15 @@
                         .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();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+        mDb2.setSchemaAsync(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(
+                mDb1.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
         List<GenericDocument> outDocuments =
                 doGet(mDb1, "namespace", "email1");
@@ -620,7 +855,7 @@
                 .setSubject("testPut example")
                 .build();
         checkIsBatchResultSuccess(
-                mDb2.put(
+                mDb2.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
         outDocuments = doGet(mDb2, "namespace", "email2");
         assertThat(outDocuments).hasSize(1);
@@ -631,7 +866,7 @@
         // change.
         Throwable failResult1 = assertThrows(
                 ExecutionException.class,
-                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+                () -> mDb1.setSchemaAsync(new SetSchemaRequest.Builder().build()).get()).getCause();
         assertThat(failResult1).isInstanceOf(AppSearchException.class);
         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
         assertThat(failResult1).hasMessageThat().contains(
@@ -639,10 +874,10 @@
 
         // 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();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
 
         // Make sure the indexed email is gone in database 1.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace")
                         .addIds("email1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
@@ -653,7 +888,7 @@
         AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3")
                 .setSubject("testPut example")
                 .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email3").getErrorMessage())
@@ -668,14 +903,14 @@
 
         // Make sure email could still be indexed in database 2.
         checkIsBatchResultSuccess(
-                mDb2.put(
+                mDb2.putAsync(
                         new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
     }
 
     @Test
     public void testGetDocuments() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -686,7 +921,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Get the document
@@ -696,7 +931,7 @@
         assertThat(outEmail).isEqualTo(inEmail);
 
         // Can't get the document in the other instance.
-        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds(
                         "id1").build()).get();
         assertThat(failResult.isSuccess()).isFalse();
@@ -709,7 +944,7 @@
     @Test
     public void testGet_addDocumentClasses() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addDocumentClasses(EmailDocument.class).build()).get();
 
         // Index a document
@@ -718,7 +953,7 @@
         inEmail.id = "id1";
         inEmail.subject = "testPut example";
         inEmail.body = "This is the body of the testPut inEmail";
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addDocuments(inEmail).build()));
 
         // Get the document
@@ -735,7 +970,7 @@
     @Test
     public void testGetDocuments_projection() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -757,7 +992,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -789,7 +1024,7 @@
     @Test
     public void testGetDocuments_projectionEmpty() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -811,7 +1046,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -836,7 +1071,7 @@
     @Test
     public void testGetDocuments_projectionNonExistentType() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -858,7 +1093,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -890,7 +1125,7 @@
     @Test
     public void testGetDocuments_wildcardProjection() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -912,7 +1147,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -945,7 +1180,7 @@
     @Test
     public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -967,7 +1202,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -993,7 +1228,7 @@
     @Test
     public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -1015,7 +1250,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -1049,7 +1284,7 @@
     @Test
     public void testQuery() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -1060,7 +1295,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
@@ -1083,7 +1318,7 @@
     @Test
     public void testQuery_getNextPage() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         Set<AppSearchEmail> emailSet = new HashSet<>();
         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
@@ -1099,7 +1334,7 @@
             emailSet.add(inEmail);
             putDocumentsRequestBuilder.addGenericDocuments(inEmail);
         }
-        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+        checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
 
         // Set number of results per page is 7.
         SearchResults searchResults = mDb1.search("body",
@@ -1114,7 +1349,7 @@
 
         // keep loading next page until it's empty.
         do {
-            results = searchResults.getNextPage().get();
+            results = searchResults.getNextPageAsync().get();
             ++pageNumber;
             for (SearchResult result : results) {
                 documents.add(result.getGenericDocument());
@@ -1129,7 +1364,7 @@
     @Test
     public void testQuery_relevanceScoring() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -1151,7 +1386,7 @@
                         .setSubject("I'm a little teapot")
                         .setBody("short and stout. Here is my handle, here is my spout.")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2).build()));
 
@@ -1198,7 +1433,7 @@
                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(genericSchema)
@@ -1214,7 +1449,7 @@
                         .build();
         GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic")
                 .setPropertyString("foo", "body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build()));
 
         // Query for the documents
@@ -1246,7 +1481,7 @@
     @Test
     public void testQuery_packageFilter() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -1257,7 +1492,7 @@
                         .setSubject("foo")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
 
         // Query for the document within our package
@@ -1273,14 +1508,14 @@
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addFilterPackageNames("some.other.package")
                 .build());
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).isEmpty();
     }
 
     @Test
     public void testQuery_namespaceFilter() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents
@@ -1298,7 +1533,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(expectedEmail, unexpectedEmail).build()));
 
@@ -1333,7 +1568,7 @@
     @Test
     public void testQuery_getPackageName() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -1344,7 +1579,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
@@ -1356,7 +1591,7 @@
         List<GenericDocument> documents = new ArrayList<>();
         // keep loading next page until it's empty.
         do {
-            results = searchResults.getNextPage().get();
+            results = searchResults.getNextPageAsync().get();
             for (SearchResult result : results) {
                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
                 assertThat(result.getPackageName()).isEqualTo(
@@ -1370,7 +1605,7 @@
     @Test
     public void testQuery_getDatabaseName() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -1381,7 +1616,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
@@ -1393,7 +1628,7 @@
         List<GenericDocument> documents = new ArrayList<>();
         // keep loading next page until it's empty.
         do {
-            results = searchResults.getNextPage().get();
+            results = searchResults.getNextPageAsync().get();
             for (SearchResult result : results) {
                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
@@ -1403,10 +1638,10 @@
         assertThat(documents).hasSize(1);
 
         // Schema registration for another database
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
@@ -1417,7 +1652,7 @@
         documents = new ArrayList<>();
         // keep loading next page until it's empty.
         do {
-            results = searchResults.getNextPage().get();
+            results = searchResults.getNextPageAsync().get();
             for (SearchResult result : results) {
                 assertThat(result.getGenericDocument()).isEqualTo(inEmail);
                 assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
@@ -1430,7 +1665,7 @@
     @Test
     public void testQuery_projection() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1465,7 +1700,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1495,7 +1730,7 @@
     @Test
     public void testQuery_projectionEmpty() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1530,7 +1765,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1558,7 +1793,7 @@
     @Test
     public void testQuery_projectionNonExistentType() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1593,7 +1828,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1624,7 +1859,7 @@
     @Test
     public void testQuery_wildcardProjection() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1658,7 +1893,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1688,7 +1923,7 @@
     @Test
     public void testQuery_wildcardProjectionEmpty() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1720,7 +1955,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1745,7 +1980,7 @@
     @Test
     public void testQuery_wildcardProjectionNonExistentType() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .addSchemas(new AppSearchSchema.Builder("Note")
@@ -1780,7 +2015,7 @@
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email, note).build()));
 
@@ -1811,9 +2046,9 @@
     @Test
     public void testQuery_twoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document to instance 1.
@@ -1824,7 +2059,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
 
         // Index a document to instance 2.
@@ -1835,7 +2070,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
 
         // Query for instance 1.
@@ -1865,7 +2100,7 @@
                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
 
         // Index a document
@@ -1874,7 +2109,7 @@
                         .setPropertyString("subject", "A commonly used fake word is foo. "
                                 + "Another nonsense word that’s used a lot is bar")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
 
         // Query for the document
@@ -1886,7 +2121,7 @@
                         .setMaxSnippetSize(10)
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .build());
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(1);
 
         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
@@ -1923,32 +2158,36 @@
                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 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())
+        checkIsBatchResultSuccess(mDb1.putAsync(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
@@ -1964,7 +2203,7 @@
                         .build());
 
         // Check result 1
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(3);
 
         assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("id2");
@@ -1998,7 +2237,7 @@
                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
 
         String japanese =
@@ -2011,7 +2250,7 @@
                 new GenericDocument.Builder<>("namespace", "id", "Generic")
                         .setPropertyString("subject", japanese)
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
 
         // Query for the document
@@ -2022,7 +2261,7 @@
                         .setSnippetCountPerProperty(1)
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .build());
-        List<SearchResult> results = searchResults.getNextPage().get();
+        List<SearchResult> results = searchResults.getNextPageAsync().get();
         assertThat(results).hasSize(1);
 
         List<SearchResult.MatchInfo> matchInfos = results.get(0).getMatchInfos();
@@ -2048,7 +2287,7 @@
     @Test
     public void testRemove() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2066,7 +2305,7 @@
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
@@ -2074,12 +2313,12 @@
         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
 
         // Delete the document
-        checkIsBatchResultSuccess(mDb1.remove(
+        checkIsBatchResultSuccess(mDb1.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace").addIds(
                         "id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
                         "id2").build())
                 .get();
@@ -2089,7 +2328,7 @@
         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
 
         // Test if we delete a nonexistent id.
-        AppSearchBatchResult<String, Void> deleteResult = mDb1.remove(
+        AppSearchBatchResult<String, Void> deleteResult = mDb1.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace").addIds(
                         "id1").build()).get();
 
@@ -2100,7 +2339,7 @@
     @Test
     public void testRemove_multipleIds() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2118,7 +2357,7 @@
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
@@ -2126,13 +2365,13 @@
         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
 
         // Delete the document
-        checkIsBatchResultSuccess(mDb1.remove(
+        checkIsBatchResultSuccess(mDb1.removeAsync(
                 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())
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
+                        new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
+                                "id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2144,7 +2383,7 @@
     @Test
     public void testRemoveByQuery() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2162,7 +2401,7 @@
                         .setSubject("bar")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
@@ -2170,10 +2409,11 @@
         assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
 
         // Delete the email 1 by query "foo"
-        mDb1.remove("foo",
+        mDb1.removeAsync("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())
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
+                        new GetByDocumentIdRequest.Builder("namespace")
+                                .addIds("id1", "id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2181,10 +2421,10 @@
         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
 
         // Delete the email 2 by query "bar"
-        mDb1.remove("bar",
+        mDb1.removeAsync("bar",
                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
-        getResult = mDb1.getByDocumentId(
-                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
+        getResult = mDb1.getByDocumentIdAsync(
+                        new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("id2").getResultCode())
@@ -2195,7 +2435,7 @@
     @Test
     public void testRemoveByQuery_nonExistNamespace() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2213,7 +2453,7 @@
                         .setSubject("bar")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
@@ -2221,7 +2461,7 @@
         assertThat(doGet(mDb1, "namespace2", "id2")).hasSize(1);
 
         // Delete the email by nonExist namespace.
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .addFilterNamespaces("nonExistNamespace").build()).get();
         // None of these emails will be deleted.
@@ -2232,7 +2472,7 @@
     @Test
     public void testRemoveByQuery_packageFilter() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2243,7 +2483,7 @@
                         .setSubject("foo")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
 
         // Check the presence of the documents
@@ -2251,17 +2491,17 @@
 
         // 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",
+        mDb1.removeAsync("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(
+        mDb1.removeAsync("foo", new SearchSpec.Builder().setTermMatch(
                 SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
                 ApplicationProvider.getApplicationContext().getPackageName()).build()).get();
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
@@ -2272,7 +2512,7 @@
     @Test
     public void testRemove_twoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2283,32 +2523,32 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 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(
+        AppSearchBatchResult<String, Void> deleteResult = mDb2.removeAsync(
                 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(
+        checkIsBatchResultSuccess(mDb1.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 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(
+        deleteResult = mDb1.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
                 AppSearchResult.RESULT_NOT_FOUND);
@@ -2318,7 +2558,7 @@
     public void testRemoveByTypes() throws Exception {
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
                         genericSchema).build()).get();
 
@@ -2339,7 +2579,7 @@
                         .build();
         GenericDocument document1 =
                 new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
                         .build()));
 
@@ -2347,7 +2587,7 @@
         assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
 
         // Delete the email type
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
@@ -2355,7 +2595,7 @@
                 .get();
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
@@ -2369,9 +2609,9 @@
     @Test
     public void testRemoveByTypes_twoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2389,9 +2629,9 @@
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
@@ -2399,7 +2639,7 @@
         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
 
         // Delete the email type in instance 1
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
@@ -2407,14 +2647,14 @@
                 .get();
 
         // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 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(
+        getResult = mDb2.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
@@ -2430,7 +2670,7 @@
                         .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
                         genericSchema).build()).get();
 
@@ -2452,7 +2692,7 @@
         GenericDocument document1 =
                 new GenericDocument.Builder<>("document", "id3", "Generic")
                         .setPropertyString("foo", "bar").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
                         .build()));
 
@@ -2461,7 +2701,7 @@
         assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
 
         // Delete the email namespace
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .addFilterNamespaces("email")
@@ -2469,7 +2709,7 @@
                 .get();
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("email")
                         .addIds("id1", "id2").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
@@ -2477,7 +2717,7 @@
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
         assertThat(getResult.getFailures().get("id2").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        getResult = mDb1.getByDocumentId(
+        getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("document")
                         .addIds("id3").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
@@ -2487,9 +2727,9 @@
     @Test
     public void testRemoveByNamespaces_twoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2507,9 +2747,9 @@
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
@@ -2517,7 +2757,7 @@
         assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
 
         // Delete the email namespace in instance 1
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .addFilterNamespaces("email")
@@ -2525,7 +2765,7 @@
                 .get();
 
         // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("email")
                         .addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
@@ -2533,7 +2773,7 @@
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Make sure it's still in instance 2.
-        getResult = mDb2.getByDocumentId(
+        getResult = mDb2.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("email")
                         .addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
@@ -2543,9 +2783,9 @@
     @Test
     public void testRemoveAll_twoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2563,9 +2803,9 @@
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
@@ -2573,21 +2813,21 @@
         assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
 
         // Delete the all document in instance 1
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 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(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 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(
+        getResult = mDb2.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
         assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
@@ -2596,9 +2836,9 @@
     @Test
     public void testRemoveAll_termMatchType() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2630,9 +2870,9 @@
                         .setSubject("testPut example 4")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build()));
 
         // Check the presence of the documents
@@ -2648,7 +2888,7 @@
         assertThat(documents).hasSize(2);
 
         // Delete the all document in instance 1 with TERM_MATCH_PREFIX
-        mDb1.remove("",
+        mDb1.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .build())
@@ -2660,7 +2900,7 @@
         assertThat(documents).isEmpty();
 
         // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
-        mDb2.remove("",
+        mDb2.removeAsync("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .build())
@@ -2675,7 +2915,7 @@
     @Test
     public void testRemoveAllAfterEmpty() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -2686,7 +2926,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
 
         // Check the presence of the documents
@@ -2694,23 +2934,22 @@
 
         // Remove the document
         checkIsBatchResultSuccess(
-                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                mDb1.removeAsync(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
                         "id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentIdAsync(
                 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();
+        mDb1.removeAsync("", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
 
         // Make sure it's still gone
-        getResult = mDb1.getByDocumentId(
+        getResult = mDb1.getByDocumentIdAsync(
                 new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("id1").getResultCode())
@@ -2720,7 +2959,7 @@
     @Test
     public void testCloseAndReopen() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -2731,12 +2970,12 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // close and re-open the appSearchSession
         mDb1.close();
-        mDb1 = createSearchSession(DB_NAME_1).get();
+        mDb1 = createSearchSessionAsync(DB_NAME_1).get();
 
         // Query for the document
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
@@ -2752,12 +2991,12 @@
         // 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(
+        AppSearchSession sameThreadDb = createSearchSessionAsync(
                 "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
 
         try {
             // Schema registration -- just mutate something
-            sameThreadDb.setSchema(
+            sameThreadDb.setSchemaAsync(
                     new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
             // Close the database. No further call will be allowed.
@@ -2773,15 +3012,16 @@
         } 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(
+            AppSearchSession reopen = createSearchSessionAsync(
                     "sameThreadDb", MoreExecutors.newDirectExecutorService()).get();
-            reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+            reopen.setSchemaAsync(new SetSchemaRequest.Builder()
+                    .setForceOverride(true).build()).get();
         }
     }
 
     @Test
     public void testReportUsage() throws Exception {
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents.
@@ -2789,19 +3029,19 @@
                 new AppSearchEmail.Builder("namespace", "id1").build();
         AppSearchEmail email2 =
                 new AppSearchEmail.Builder("namespace", "id2").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 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")
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
                 .setUsageTimestampMillis(1000).build()).get();
-        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
                 .setUsageTimestampMillis(2000).build()).get();
-        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1")
                 .setUsageTimestampMillis(3000).build()).get();
-        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
                 .setUsageTimestampMillis(10000).build()).get();
-        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id2")
                 .setUsageTimestampMillis(20000).build()).get();
 
         // Query by number of usages
@@ -2832,19 +3072,19 @@
 
     @Test
     public void testReportUsage_invalidNamespace() throws Exception {
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "id1").build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
 
         // Use the correct namespace; it works
-        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
+        mDb1.reportUsageAsync(new ReportUsageRequest.Builder("namespace", "id1").build()).get();
 
         // Use an incorrect namespace; it fails
         ExecutionException e = assertThrows(
                 ExecutionException.class,
-                () -> mDb1.reportUsage(
+                () -> mDb1.reportUsageAsync(
                         new ReportUsageRequest.Builder("namespace2", "id1").build()).get());
         assertThat(e).hasCauseThat().isInstanceOf(AppSearchException.class);
         AppSearchException cause = (AppSearchException) e.getCause();
@@ -2853,26 +3093,26 @@
 
     @Test
     public void testGetStorageInfo() throws Exception {
-        StorageInfo storageInfo = mDb1.getStorageInfo().get();
+        StorageInfo storageInfo = mDb1.getStorageInfoAsync().get();
         assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
 
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Still no storage space attributed with just a schema
-        storageInfo = mDb1.getStorageInfo().get();
+        storageInfo = mDb1.getStorageInfoAsync().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(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2,
                         email3).build()));
 
         // Non-zero size now
-        storageInfo = mDb1.getStorageInfo().get();
+        storageInfo = mDb1.getStorageInfoAsync().get();
         assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
         assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
         assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
@@ -2881,7 +3121,7 @@
     @Test
     public void testFlush() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -2892,19 +3132,19 @@
                 .setBody("This is the body of the testPut email")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
                 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();
+        mDb1.requestFlushAsync().get();
     }
 
     @Test
     public void testQuery_ResultGroupingLimits() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index four documents.
@@ -2915,7 +3155,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
         AppSearchEmail inEmail2 =
                 new AppSearchEmail.Builder("namespace1", "id2")
@@ -2924,7 +3164,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
         AppSearchEmail inEmail3 =
                 new AppSearchEmail.Builder("namespace2", "id3")
@@ -2933,7 +3173,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
         AppSearchEmail inEmail4 =
                 new AppSearchEmail.Builder("namespace2", "id4")
@@ -2942,7 +3182,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
 
         // Query with per package result grouping. Only the last document 'email4' should be
@@ -2979,7 +3219,7 @@
     @Test
     public void testIndexNestedDocuments() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA)
                 .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
                         .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
@@ -3008,7 +3248,7 @@
                 new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
                         .setPropertyDocument("prop", email)
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
                 .addGenericDocuments(yesNestedIndex, noNestedIndex).build()));
 
         // Query.
@@ -3017,7 +3257,7 @@
                 .setSnippetCount(10)
                 .setSnippetCountPerProperty(10)
                 .build());
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
         assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
         List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
@@ -3030,7 +3270,7 @@
     @Test
     public void testCJKTQuery() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document to instance 1.
@@ -3038,7 +3278,7 @@
                 new AppSearchEmail.Builder("namespace", "uri1")
                         .setBody("他是個男孩 is a boy")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
 
         // Query for "ä»–" (He)
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index 8d45573..51745dd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -18,6 +18,7 @@
 
 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -30,6 +31,7 @@
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SearchResult;
@@ -53,33 +55,51 @@
 
 public class AppSearchSessionLocalCtsTest extends AppSearchSessionCtsTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName, @NonNull ExecutorService executor) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, dbName)
                         .setWorkerExecutor(executor).build());
     }
 
+    @Test
+    public void testFeaturesSupported() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
+                new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
+
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)).isTrue();
+    }
+
     // TODO(b/194207451) This test can be moved to CtsTestBase if customized logger is
     //  supported for platform backend.
     @Test
     public void testLogger_searchStatsLogged_forEmptyFirstPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -97,7 +117,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
         assertThat(logger.mSearchStats).isNull();
@@ -111,7 +131,7 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(0);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -133,12 +153,12 @@
     public void testLogger_searchStatsLogged_forNonEmptyFirstPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -156,7 +176,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
         assertThat(logger.mSearchStats).isNull();
@@ -170,7 +190,7 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(2);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -192,12 +212,12 @@
     public void testLogger_searchStatsLogged_forEmptySecondPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -215,7 +235,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
         // Query for the document
@@ -228,12 +248,12 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(2);
 
         // Get second(empty) page
         logger.mSearchStats = null;
-        page = searchResults.getNextPage().get();
+        page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(0);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -255,12 +275,12 @@
     public void testLogger_searchStatsLogged_forNonEmptySecondPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -278,7 +298,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
         // Query for the document
@@ -291,12 +311,12 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
 
         // Get second page
         logger.mSearchStats = null;
-        page = searchResults.getNextPage().get();
+        page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -318,7 +338,7 @@
     public void testSetSchemaStats_withoutSchemaMigration() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
         AppSearchSchema appSearchSchema = new AppSearchSchema.Builder("testSchema")
@@ -330,7 +350,7 @@
                         .build())
                 .build();
 
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(appSearchSchema).build()).get();
 
         assertThat(logger.mSetSchemaStats).isNotNull();
@@ -352,7 +372,7 @@
     public void testSetSchemaStats_withSchemaMigration() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
         AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
@@ -407,11 +427,11 @@
             }
         };
 
-        db2.setSchema(new SetSchemaRequest.Builder().addSchemas(
+        db2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(
                 schema).setForceOverride(true).build()).get();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
-        db2.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+        db2.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(newSchema)
                 .setMigrator("testSchema", migrator)
                 .setVersion(2)     // upgrade version
                 .build()).get();
@@ -424,16 +444,15 @@
         assertThat(schemaMigrationStats.getSavedDocumentCount()).isEqualTo(1);
     }
 
-    // TODO(b/185441119) Following test can be moved to CtsTestBase if we fix the binder
-    //  transaction limit in framework.
+    // Framework has max Document size which is 512KiB, this test should only exists in Jetpack.
     @Test
-    public void testPutLargeDocument() throws Exception {
+    public void testPutLargeDocumentToIcing() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         char[] chars = new char[16_000_000];
@@ -447,7 +466,7 @@
                 .setSubject("testPut example")
                 .setBody(body)
                 .build();
-        AppSearchBatchResult<String, Void> result = db2.put(
+        AppSearchBatchResult<String, Void> result = db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
         assertThat(result.isSuccess()).isTrue();
 
@@ -460,16 +479,15 @@
         assertThat(outEmail).isEqualTo(email);
     }
 
-    // TODO(b/185441119) Following test can be moved to CtsTestBase if we fix the binder
-    //  transaction limit in framework.
+    // Framework has max Document size which is 512KiB, this test should only exists in Jetpack.
     @Test
-    public void testPutLargeDocument_exceedLimit() throws Exception {
+    public void testPutLargeDocumentToIcing_exceedLimit() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Create a String property that make the document exceed the total size limit.
@@ -483,7 +501,7 @@
                 .setSubject("testPut example")
                 .setBody(body)
                 .build();
-        AppSearchBatchResult<String, Void> result = db2.put(
+        AppSearchBatchResult<String, Void> result = db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
         assertThat(result.getFailures()).containsKey("id1");
         assertThat(result.getFailures().get("id1").getErrorMessage())
@@ -491,12 +509,42 @@
     }
 
     @Test
-    public void testCapabilities() throws Exception {
+    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
+        AppSearchSession db = LocalStorage.createSearchSessionAsync(
+                new LocalStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+        db.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
 
-        assertThat(db2.getFeatures().isFeatureSupported(
-                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
+        // Index a document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyBytes("bytes")
+                .setPropertyDocument("document")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(db.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1")
+                .build();
+        List<GenericDocument> outDocuments = doGet(db, request);
+        assertThat(outDocuments).hasSize(1);
+        GenericDocument outDocument = outDocuments.get(0);
+        assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
+        assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
index a0cd21a..57681de 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
@@ -16,15 +16,28 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.cts.app;
 
+import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeTrue;
+
 import android.content.Context;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SdkSuppress;
 
@@ -32,35 +45,102 @@
 
 import org.junit.Test;
 
+import java.util.List;
 import java.util.concurrent.ExecutorService;
 
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AppSearchSessionPlatformCtsTest extends AppSearchSessionCtsTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createSearchSession(
+        return PlatformStorage.createSearchSessionAsync(
                 new PlatformStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName, @NonNull ExecutorService executor) {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createSearchSession(
+        return PlatformStorage.createSearchSessionAsync(
                 new PlatformStorage.SearchContext.Builder(context, dbName)
                         .setWorkerExecutor(executor).build());
     }
 
     @Test
-    public void testCapabilities() throws Exception {
+    public void testFeaturesSupported() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = PlatformStorage.createSearchSession(
+        AppSearchSession db2 = PlatformStorage.createSearchSessionAsync(
                 new PlatformStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
-
-        // TODO(b/201316758) Update to reflect support in Android T+ once this feature is synced
-        // over into service-appsearch.
         assertThat(db2.getFeatures().isFeatureSupported(
-                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isFalse();
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH))
+                .isEqualTo(BuildCompat.isAtLeastT());
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK))
+                .isEqualTo(BuildCompat.isAtLeastT());
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA))
+                .isEqualTo(BuildCompat.isAtLeastT());
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_GET_BY_ID))
+                .isEqualTo(BuildCompat.isAtLeastT());
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY))
+                .isEqualTo(BuildCompat.isAtLeastT());
+    }
+
+    @Test
+    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchSession db = PlatformStorage.createSearchSessionAsync(
+                new PlatformStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+        db.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyBytes("bytes")
+                .setPropertyDocument("document")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(db.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1")
+                .build();
+        List<GenericDocument> outDocuments = doGet(db, request);
+        assertThat(outDocuments).hasSize(1);
+        GenericDocument outDocument = outDocuments.get(0);
+        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
+                || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) {
+            // We fixed b/204677124 in Android T, so in S and S_V2, getByteArray and
+            // getDocumentArray will return null if we set empty properties.
+            assertThat(outDocument.getPropertyBytesArray("bytes")).isNull();
+            assertThat(outDocument.getPropertyDocumentArray("document")).isNull();
+        } else {
+            assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
+            assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
+        }
+    }
+
+    @Override
+    @Test
+    public void testPutLargeDocumentBatch() throws Exception {
+        // b/185441119 was fixed in Android T, this test will fail on S_V2 devices and below.
+        assumeTrue(BuildCompat.isAtLeastT());
+        super.testPutLargeDocumentBatch();
     }
 }
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
index 1220731..72faaef 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
@@ -20,12 +20,24 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+
+import com.google.common.collect.ImmutableSet;
 
 import org.junit.Test;
 
+import java.util.Arrays;
+
 public class GetSchemaResponseCtsTest {
     @Test
     public void testRebuild() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
         AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
@@ -44,16 +56,111 @@
                 ).build();
 
         GetSchemaResponse.Builder builder =
-                new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1);
+                new GetSchemaResponse.Builder().setVersion(42).addSchema(schema1)
+                        .addSchemaTypeNotDisplayedBySystem("Email1")
+                        .setSchemaTypeVisibleToPackages("Email1",
+                                ImmutableSet.of(packageIdentifier1))
+                        .setRequiredPermissionsForSchemaTypeVisibility("Email1",
+                                ImmutableSet.of(
+                                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                                SetSchemaRequest.READ_CALENDAR),
+                                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                        );
 
         GetSchemaResponse original = builder.build();
-        GetSchemaResponse rebuild = builder.setVersion(37).addSchema(schema2).build();
+        GetSchemaResponse rebuild = builder.setVersion(37).addSchema(schema2)
+                .addSchemaTypeNotDisplayedBySystem("Email2")
+                .setSchemaTypeVisibleToPackages("Email2",
+                        ImmutableSet.of(packageIdentifier2))
+                .setRequiredPermissionsForSchemaTypeVisibility("Email2",
+                        ImmutableSet.of(
+                                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                                        ImmutableSet.of(SetSchemaRequest
+                                                .READ_ASSISTANT_APP_SEARCH_DATA))
+                ).build();
 
         // rebuild won't effect the original object
         assertThat(original.getVersion()).isEqualTo(42);
         assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly("Email1");
+        assertThat(original.getSchemaTypesVisibleToPackages()).hasSize(1);
+        assertThat(original.getSchemaTypesVisibleToPackages().get("Email1"))
+                .containsExactly(packageIdentifier1);
+        assertThat(original.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+                "Email1",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
 
         assertThat(rebuild.getVersion()).isEqualTo(37);
         assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(rebuild.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly("Email1", "Email2");
+        assertThat(rebuild.getSchemaTypesVisibleToPackages()).hasSize(2);
+        assertThat(rebuild.getSchemaTypesVisibleToPackages().get("Email1"))
+                .containsExactly(packageIdentifier1);
+        assertThat(rebuild.getSchemaTypesVisibleToPackages().get("Email2"))
+                .containsExactly(packageIdentifier2);
+        assertThat(rebuild.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+                "Email1",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                "Email2",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                        ImmutableSet.of(SetSchemaRequest
+                                .READ_ASSISTANT_APP_SEARCH_DATA)));
+    }
+
+    @Test
+    public void setVisibility() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .addSchemaTypeNotDisplayedBySystem("Email")
+                        .addSchemaTypeNotDisplayedBySystem("Text")
+                        .setSchemaTypeVisibleToPackages("Email",
+                                ImmutableSet.of(packageIdentifier1, packageIdentifier2))
+                        .setRequiredPermissionsForSchemaTypeVisibility("Email",
+                                ImmutableSet.of(
+                                                ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                                        SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                                                ImmutableSet.of(SetSchemaRequest
+                                                        .READ_ASSISTANT_APP_SEARCH_DATA)))
+                        .build();
+
+        assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly("Email", "Text");
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()).hasSize(1);
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages().get("Email"))
+                .containsExactly(packageIdentifier1, packageIdentifier2);
+        assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility().get("Email"))
+                .containsExactlyElementsIn(ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                        ImmutableSet.of(SetSchemaRequest
+                                .READ_ASSISTANT_APP_SEARCH_DATA)));
+    }
+
+    @Test
+    public void getEmptyVisibility() {
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .build();
+        assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages()).isEmpty();
+        assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
     }
 }
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
index 1e03d4e..e9458da 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -28,12 +28,15 @@
 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.AppSearchSession;
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
@@ -45,11 +48,13 @@
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.observer.DocumentChangeInfo;
 import androidx.appsearch.observer.ObserverSpec;
+import androidx.appsearch.observer.SchemaChangeInfo;
 import androidx.appsearch.testutil.AppSearchEmail;
 import androidx.appsearch.testutil.TestObserverCallback;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.After;
@@ -75,21 +80,21 @@
 
     protected GlobalSearchSession mGlobalSearchSession;
 
-    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+    protected abstract ListenableFuture<AppSearchSession> createSearchSessionAsync(
             @NonNull String dbName);
 
-    protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSession();
+    protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync();
 
     @Before
     public void setUp() throws Exception {
-        mDb1 = createSearchSession(DB_NAME_1).get();
-        mDb2 = createSearchSession(DB_NAME_2).get();
+        mDb1 = createSearchSessionAsync(DB_NAME_1).get();
+        mDb2 = createSearchSessionAsync(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();
 
-        mGlobalSearchSession = createGlobalSearchSession().get();
+        mGlobalSearchSession = createGlobalSearchSessionAsync().get();
     }
 
     @After
@@ -99,9 +104,9 @@
     }
 
     private void cleanup() throws Exception {
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
@@ -128,6 +133,48 @@
     }
 
     @Test
+    public void testGlobalGetById() throws Exception {
+        assumeTrue(mGlobalSearchSession.getFeatures().isFeatureSupported(
+                Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
+        SearchSpec exactSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        AppSearchBatchResult<String, GenericDocument> nonExistent =
+                mGlobalSearchSession.getByDocumentIdAsync(mContext.getPackageName(), DB_NAME_1,
+                        new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+                                .build()).get();
+
+        assertThat(nonExistent.isSuccess()).isFalse();
+        assertThat(nonExistent.getSuccesses()).isEmpty();
+        assertThat(nonExistent.getFailures()).containsKey("id1");
+        assertThat(nonExistent.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+
+        // 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.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        AppSearchBatchResult<String, GenericDocument> afterPutDocuments =
+                mGlobalSearchSession.getByDocumentIdAsync(mContext.getPackageName(), DB_NAME_1,
+                        new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+                                .build()).get();
+        assertThat(afterPutDocuments.getSuccesses()).containsExactly("id1", inEmail);
+    }
+
+    @Test
     public void testGlobalQuery_oneInstance() throws Exception {
         // Snapshot what documents may already exist on the device.
         SearchSpec exactSearchSpec = new SearchSpec.Builder()
@@ -138,7 +185,7 @@
                 exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
@@ -149,7 +196,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
@@ -173,9 +220,9 @@
         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document to instance 1.
@@ -186,7 +233,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
 
         // Index a document to instance 2.
@@ -197,7 +244,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
 
         // Query across all instances
@@ -215,7 +262,7 @@
         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         List<AppSearchEmail> emailList = new ArrayList<>();
         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
@@ -232,7 +279,7 @@
             emailList.add(inEmail);
             putDocumentsRequestBuilder.addGenericDocuments(inEmail);
         }
-        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
+        checkIsBatchResultSuccess(mDb1.putAsync(putDocumentsRequestBuilder.build()));
 
         // Set number of results per page is 7.
         int pageSize = 7;
@@ -248,7 +295,7 @@
 
         // keep loading next page until it's empty.
         do {
-            results = searchResults.getNextPage().get();
+            results = searchResults.getNextPageAsync().get();
             ++pageNumber;
             for (SearchResult result : results) {
                 documents.add(result.getGenericDocument());
@@ -293,19 +340,19 @@
                 ).build();
 
         // db1 has both "Generic" and "builtin:Email"
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(genericSchema).addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // db2 only has "builtin:Email"
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 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(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(genericDocument).build()));
 
@@ -318,9 +365,9 @@
                         .build();
 
         // Put the email in both databases
-        checkIsBatchResultSuccess((mDb1.put(
+        checkIsBatchResultSuccess((mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
 
         // Query for all documents across types
@@ -352,9 +399,9 @@
                 exactNamespace1SearchSpec);
 
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents
@@ -365,7 +412,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(document1).build()));
 
@@ -376,7 +423,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
 
         // Query for all namespaces
@@ -410,9 +457,9 @@
                 testPackageSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents
@@ -423,7 +470,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(document1).build()));
 
@@ -434,7 +481,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
 
         // Query in some other package
@@ -454,11 +501,11 @@
     @Test
     public void testGlobalQuery_projectionTwoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -472,7 +519,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1).build()));
 
@@ -484,7 +531,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email2).build()));
 
@@ -516,11 +563,11 @@
     @Test
     public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -534,7 +581,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1).build()));
 
@@ -546,7 +593,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email2).build()));
 
@@ -573,11 +620,11 @@
     @Test
     public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
         // Schema registration
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
-        mDb2.setSchema(
+        mDb2.setSchemaAsync(
                 new SetSchemaRequest.Builder()
                         .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
@@ -591,7 +638,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1).build()));
 
@@ -603,7 +650,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email2).build()));
 
@@ -636,9 +683,9 @@
     @Test
     public void testQuery_ResultGroupingLimits() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index one document in 'namespace1' and one document in 'namespace2' into db1.
@@ -649,7 +696,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
         AppSearchEmail inEmail2 =
                 new AppSearchEmail.Builder("namespace2", "id2")
@@ -658,7 +705,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.put(
+        checkIsBatchResultSuccess(mDb1.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
 
         // Index one document in 'namespace1' and one document in 'namespace2' into db2.
@@ -669,7 +716,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
         AppSearchEmail inEmail4 =
                 new AppSearchEmail.Builder("namespace2", "id4")
@@ -678,7 +725,7 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.put(
+        checkIsBatchResultSuccess(mDb2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
 
         // Query with per package result grouping. Only the last document 'email4' should be
@@ -716,7 +763,7 @@
     @Test
     public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
         // Index a document
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         AppSearchEmail email1 =
                 new AppSearchEmail.Builder("namespace", "id1")
@@ -727,7 +774,8 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
 
         // Query
         List<SearchResult> page;
@@ -735,13 +783,13 @@
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
                 .build())) {
-            page = results.getNextPage().get();
+            page = results.getNextPageAsync().get();
         }
         assertThat(page).isNotEmpty();
         SearchResult firstResult = page.get(0);
 
         ExecutionException exception = assertThrows(
-                ExecutionException.class, () -> mGlobalSearchSession.reportSystemUsage(
+                ExecutionException.class, () -> mGlobalSearchSession.reportSystemUsageAsync(
                         new ReportSystemUsageRequest.Builder(
                                 firstResult.getPackageName(),
                                 firstResult.getDatabaseName(),
@@ -758,56 +806,63 @@
     @Test
     public void testAddObserver_notSupported() {
         assumeFalse(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
         assertThrows(
                 UnsupportedOperationException.class,
-                () -> mGlobalSearchSession.addObserver(
+                () -> mGlobalSearchSession.registerObserverCallback(
                         mContext.getPackageName(),
                         new ObserverSpec.Builder().build(),
                         EXECUTOR,
                         new TestObserverCallback()));
         assertThrows(
                 UnsupportedOperationException.class,
-                () -> mGlobalSearchSession.removeObserver(
+                () -> mGlobalSearchSession.unregisterObserverCallback(
                         mContext.getPackageName(), new TestObserverCallback()));
     }
 
     @Test
     public void testAddObserver() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback observer = new TestObserverCallback();
 
         // Register observer. Note: the type does NOT exist yet!
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
                 observer);
 
         // Index a document
-        mDb1.setSchema(
+        mDb1.setSchemaAsync(
                 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()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
 
         // Make sure the notification was received.
-        observer.waitForNotificationCount(1);
-        assertThat(observer.getSchemaChanges()).isEmpty();
+        observer.waitForNotificationCount(2);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(),
+                        DB_NAME_1,
+                        /*changedSchemaNames=*/ImmutableSet.of(AppSearchEmail.SCHEMA_TYPE)));
         assertThat(observer.getDocumentChanges()).containsExactly(
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE));
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1"))
+        );
     }
 
     @Test
     public void testRegisterObserver_MultiType() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback unfilteredObserver = new TestObserverCallback();
         TestObserverCallback emailObserver = new TestObserverCallback();
@@ -816,18 +871,18 @@
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Register two observers. One has no filters, the other filters on email.
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 EXECUTOR,
                 unfilteredObserver);
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
@@ -845,14 +900,17 @@
                 "namespace2", "id2", "Gift").build();
 
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, gift1).build()));
         checkIsBatchResultSuccess(
-                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+                mDb2.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(gift1).build()));
 
         // Make sure the notification was received.
         unfilteredObserver.waitForNotificationCount(5);
@@ -864,27 +922,32 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace2",
-                        "Gift"),
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id2")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace2",
-                        "Gift")
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id2"))
         );
 
         // Check the filtered observer
@@ -894,24 +957,27 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE)
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1"))
         );
     }
 
     @Test
     public void testRegisterObserver_removeById() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback unfilteredObserver = new TestObserverCallback();
         TestObserverCallback emailObserver = new TestObserverCallback();
@@ -920,14 +986,14 @@
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
 
         // Register two observers. One, registered later, has no filters. The other, registered
         // now, filters on email.
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
@@ -945,27 +1011,29 @@
                 "namespace2", "id2", "Gift").build();
 
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, gift1).build()));
         checkIsBatchResultSuccess(
-                mDb2.put(new PutDocumentsRequest.Builder()
+                mDb2.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, gift1).build()));
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(gift1).build()));
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(gift1).build()));
 
         // Register the second observer
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 EXECUTOR,
                 unfilteredObserver);
 
         // Remove some of the documents.
-        checkIsBatchResultSuccess(mDb1.remove(
+        checkIsBatchResultSuccess(mDb1.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
-        checkIsBatchResultSuccess(mDb2.remove(
+        checkIsBatchResultSuccess(mDb2.removeAsync(
                 new RemoveByDocumentIdRequest.Builder("namespace2").addIds("id2").build()));
 
         // Make sure the notification was received. emailObserver should have seen:
@@ -981,22 +1049,26 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE)
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1"))
         );
 
         // Check unfilteredObserver
@@ -1006,19 +1078,21 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace2",
-                        "Gift")
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id2"))
         );
     }
 
     @Test
     public void testRegisterObserver_removeByQuery() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback unfilteredObserver = new TestObserverCallback();
         TestObserverCallback emailObserver = new TestObserverCallback();
@@ -1027,9 +1101,9 @@
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
 
         // Index some documents
@@ -1040,19 +1114,19 @@
                 "namespace2", "id3", "Gift").build();
 
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, email2, gift1).build()));
         checkIsBatchResultSuccess(
-                mDb2.put(new PutDocumentsRequest.Builder().addGenericDocuments(
+                mDb2.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(
                         email1, email2, gift1).build()));
 
         // Register observers
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 EXECUTOR,
                 unfilteredObserver);
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
@@ -1065,20 +1139,16 @@
         assertThat(emailObserver.getDocumentChanges()).isEmpty();
 
         // Remove "cat" emails in db1 and all types in db2
-        mDb1.remove(
-                "cat",
-                new SearchSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build())
+        mDb1.removeAsync("cat",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build())
                 .get();
-        mDb2.remove("", new SearchSpec.Builder().build()).get();
+        mDb2.removeAsync("", new SearchSpec.Builder().build()).get();
 
         // Make sure the notification was received. UnfilteredObserver should have seen:
         //   -db1:id2, -db2:id1, -db2:id2, -db2:id3
         // emailObserver should have seen:
         //   -db1:id2, -db2:id1, -db2:id2
-        // TODO(b/193494000): Notifications are currently grouped by
-        //  (package, database, namespace, schema). This causes -db2:id1 and -db2:id2 to be combined
-        //  into one notification. Once notifications have IDs, we need to check to make sure all
-        //  the individual IDs are reported in the combined notification.
         unfilteredObserver.waitForNotificationCount(3);
         emailObserver.waitForNotificationCount(2);
 
@@ -1088,17 +1158,20 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id2")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1", "id2")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace2",
-                        "Gift")
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id3"))
         );
 
         // Check emailObserver
@@ -1108,19 +1181,21 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id2")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_2,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE)
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1", "id2"))
         );
     }
 
     @Test
     public void testRegisterObserver_sameCallback_differentSpecs() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback observer = new TestObserverCallback();
 
@@ -1128,16 +1203,16 @@
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
 
         // Register the same observer twice: once for gift, once for email
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
                 EXECUTOR,
                 observer);
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
@@ -1149,7 +1224,7 @@
                 "namespace2", "id3", "Gift").build();
 
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, gift1).build()));
 
         // Make sure the same observer received both values
@@ -1160,19 +1235,21 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace2",
-                        "Gift")
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id3"))
         );
     }
 
     @Test
     public void testRemoveObserver() throws Exception {
         assumeTrue(mGlobalSearchSession.getFeatures()
-                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER));
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
 
         TestObserverCallback temporaryObserver = new TestObserverCallback();
         TestObserverCallback permanentObserver = new TestObserverCallback();
@@ -1181,24 +1258,24 @@
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
                 .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("price").build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder()
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder()
+        mDb2.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(AppSearchEmail.SCHEMA, giftSchema).build()).get();
 
         // Register both observers. temporaryObserver is registered twice to ensure both instances
         // get removed.
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas(AppSearchEmail.SCHEMA_TYPE).build(),
                 EXECUTOR,
                 temporaryObserver);
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas("Gift").build(),
                 EXECUTOR,
                 temporaryObserver);
-        mGlobalSearchSession.addObserver(
+        mGlobalSearchSession.registerObserverCallback(
                 mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 EXECUTOR,
@@ -1220,7 +1297,7 @@
                 "namespace3", "id4", "Gift").build();
 
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email1, gift1).build()));
 
         // Make sure the notifications were received.
@@ -1232,12 +1309,14 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace2",
-                        "Gift"));
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id3")));
         assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
         assertThat(temporaryObserver.getDocumentChanges())
                 .containsExactlyElementsIn(expectedChangesOrig);
@@ -1246,11 +1325,12 @@
                 .containsExactlyElementsIn(expectedChangesOrig);
 
         // Unregister temporaryObserver
-        mGlobalSearchSession.removeObserver(mContext.getPackageName(), temporaryObserver);
+        mGlobalSearchSession.unregisterObserverCallback(
+                mContext.getPackageName(), temporaryObserver);
 
         // Index some more documents
         checkIsBatchResultSuccess(
-                mDb1.put(new PutDocumentsRequest.Builder()
+                mDb1.putAsync(new PutDocumentsRequest.Builder()
                         .addGenericDocuments(email2, gift2).build()));
 
         // Only the permanent observer should have received this
@@ -1263,25 +1343,300 @@
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id1")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace2",
-                        "Gift"),
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id3")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace",
-                        AppSearchEmail.SCHEMA_TYPE),
+                        AppSearchEmail.SCHEMA_TYPE,
+                        /*changedDocumentIds=*/ImmutableSet.of("id2")),
                 new DocumentChangeInfo(
                         mContext.getPackageName(),
                         DB_NAME_1,
                         "namespace3",
-                        "Gift")
+                        "Gift",
+                        /*changedDocumentIds=*/ImmutableSet.of("id4"))
         );
         assertThat(temporaryObserver.getSchemaChanges()).isEmpty();
         assertThat(temporaryObserver.getDocumentChanges())
                 .containsExactlyElementsIn(expectedChangesOrig);
     }
+
+    @Test
+    public void testGlobalGetSchema() throws Exception {
+        assumeTrue(mGlobalSearchSession.getFeatures()
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
+
+        // One schema should be set with global access and the other should be set with local
+        // access.
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(
+                        AppSearchEmail.SCHEMA).setSchemaTypeDisplayedBySystem(
+                        AppSearchEmail.SCHEMA_TYPE, /*displayed=*/false).build()).get();
+
+        GetSchemaResponse response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(),
+                DB_NAME_1).get();
+        assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+
+        response = mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_2).get();
+        assertThat(response.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+
+        // A request for a db that doesn't exist should return a response with no schemas.
+        response = mGlobalSearchSession.getSchemaAsync(
+                mContext.getPackageName(), "NonexistentDb").get();
+        assertThat(response.getSchemas()).isEmpty();
+    }
+
+    @Test
+    public void testGlobalGetSchema_notSupported() throws Exception {
+        assumeFalse(mGlobalSearchSession.getFeatures()
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA));
+
+        // One schema should be set with global access and the other should be set with local
+        // access.
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class,
+                () -> mGlobalSearchSession.getSchemaAsync(mContext.getPackageName(), DB_NAME_1));
+        assertThat(e).hasMessageThat().isEqualTo(Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA
+                + " is not supported on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testGlobalGetByDocumentId_notSupported() throws Exception {
+        assumeFalse(mGlobalSearchSession.getFeatures()
+                .isFeatureSupported(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID));
+
+        Context context = ApplicationProvider.getApplicationContext();
+
+        UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class,
+                () -> mGlobalSearchSession.getByDocumentIdAsync(context.getPackageName(), DB_NAME_1,
+                        new GetByDocumentIdRequest.Builder("namespace")
+                                .addIds("id").build()));
+
+        assertThat(e).hasMessageThat().isEqualTo(Features.GLOBAL_SEARCH_SESSION_GET_BY_ID
+                + " is not supported on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_added() throws Exception {
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mGlobalSearchSession.registerObserverCallback(
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                EXECUTOR,
+                observer);
+
+        // Add a schema type
+        assertThat(observer.getSchemaChanges()).isEmpty();
+        assertThat(observer.getDocumentChanges()).isEmpty();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(new AppSearchSchema.Builder("Type1").build())
+                                .build())
+                .get();
+
+        observer.waitForNotificationCount(1);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(),
+                        DB_NAME_1,
+                        ImmutableSet.of("Type1")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+
+        // Add two more schema types without touching the existing one
+        observer.clear();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Type1").build(),
+                                        new AppSearchSchema.Builder("Type2").build(),
+                                        new AppSearchSchema.Builder("Type3").build())
+                                .build())
+                .get();
+
+        observer.waitForNotificationCount(1);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2", "Type3")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_removed() throws Exception {
+        // Add a schema type
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(
+                                        new AppSearchSchema.Builder("Type1").build(),
+                                        new AppSearchSchema.Builder("Type2").build())
+                                .build())
+                .get();
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mGlobalSearchSession.registerObserverCallback(
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                EXECUTOR,
+                observer);
+
+        // Remove Type2
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(new AppSearchSchema.Builder("Type1").build())
+                                .setForceOverride(true)
+                                .build())
+                .get();
+
+        observer.waitForNotificationCount(1);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_contents() throws Exception {
+        // Add a schema
+        mDb1.setSchemaAsync(
+            new SetSchemaRequest.Builder()
+                    .addSchemas(
+                            new AppSearchSchema.Builder("Type1").build(),
+                            new AppSearchSchema.Builder("Type2")
+                                    .addProperty(
+                                            new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                    "booleanProp")
+                                                    .setCardinality(
+                                                            PropertyConfig.CARDINALITY_REQUIRED)
+                                                    .build())
+                                    .build())
+                    .build())
+            .get();
+
+        // Register an observer
+        TestObserverCallback observer = new TestObserverCallback();
+        mGlobalSearchSession.registerObserverCallback(
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().build(),
+                EXECUTOR,
+                observer);
+
+        // Update the schema, but don't make any actual changes
+        mDb1.setSchemaAsync(
+            new SetSchemaRequest.Builder()
+                    .addSchemas(
+                            new AppSearchSchema.Builder("Type1").build(),
+                            new AppSearchSchema.Builder("Type2")
+                                    .addProperty(
+                                            new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                    "booleanProp")
+                                                    .setCardinality(
+                                                            PropertyConfig.CARDINALITY_REQUIRED)
+                                                    .build())
+                                    .build())
+                    .build())
+            .get();
+
+        // Now update the schema again, but this time actually make a change (cardinality of the
+        // property)
+        mDb1.setSchemaAsync(
+            new SetSchemaRequest.Builder()
+                    .addSchemas(
+                            new AppSearchSchema.Builder("Type1").build(),
+                            new AppSearchSchema.Builder("Type2")
+                                    .addProperty(
+                                            new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                    "booleanProp")
+                                                    .setCardinality(
+                                                            PropertyConfig.CARDINALITY_OPTIONAL)
+                                                    .build())
+                                    .build())
+                    .build())
+            .get();
+
+        // Dispatch notifications
+        observer.waitForNotificationCount(1);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    @Test
+    public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+        // Add a schema
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(
+                                new AppSearchSchema.Builder("Type1")
+                                        .addProperty(
+                                                new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                        "booleanProp")
+                                                        .setCardinality(
+                                                                PropertyConfig.CARDINALITY_REQUIRED)
+                                                        .build())
+                                        .build(),
+                                new AppSearchSchema.Builder("Type2")
+                                        .addProperty(
+                                                new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                        "booleanProp")
+                                                        .setCardinality(
+                                                                PropertyConfig.CARDINALITY_REQUIRED)
+                                                        .build())
+                                        .build())
+                        .build())
+                .get();
+
+        // Register an observer that only listens for Type2
+        TestObserverCallback observer = new TestObserverCallback();
+        mGlobalSearchSession.registerObserverCallback(
+                /*targetPackageName=*/mContext.getPackageName(),
+                new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
+                EXECUTOR,
+                observer);
+
+        // Update both types of the schema (changed cardinalities)
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(
+                                new AppSearchSchema.Builder("Type1")
+                                        .addProperty(
+                                                new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                        "booleanProp")
+                                                        .setCardinality(
+                                                                PropertyConfig.CARDINALITY_OPTIONAL)
+                                                        .build())
+                                        .build(),
+                                new AppSearchSchema.Builder("Type2")
+                                        .addProperty(
+                                                new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                                        "booleanProp")
+                                                        .setCardinality(
+                                                                PropertyConfig.CARDINALITY_OPTIONAL)
+                                                        .build())
+                                        .build())
+                        .build())
+                .get();
+
+        observer.waitForNotificationCount(1);
+        assertThat(observer.getSchemaChanges()).containsExactly(
+                new SchemaChangeInfo(
+                        mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type2")));
+        assertThat(observer.getDocumentChanges()).isEmpty();
+    }
+
+    // TODO(b/193494000): Properly handle change notification during schema migration, and add tests
+    // for it.
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
index 19af478..a406841 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionLocalCtsTest.java
@@ -23,9 +23,11 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SearchResult;
@@ -46,25 +48,29 @@
 
 public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createSearchSession(
+        return LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
-    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync() {
         Context context = ApplicationProvider.getApplicationContext();
-        return LocalStorage.createGlobalSearchSession(
+        return LocalStorage.createGlobalSearchSessionAsync(
                 new LocalStorage.GlobalSearchContext.Builder(context).build());
     }
 
     @Test
-    public void testFeaturesSupported() {
-        assertThat(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
-        assertThat(mDb1.getFeatures().isFeatureSupported(
-                Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)).isTrue();
+    public void testGlobalGetById_jetpackMultiPackage() throws Exception {
+        // Can't global get document in different package in jetpack
+        AppSearchBatchResult<String, GenericDocument> fakePackage =
+                mGlobalSearchSession.getByDocumentIdAsync("fake", DB_NAME_1,
+                        new GetByDocumentIdRequest.Builder("namespace").addIds("id1")
+                                .build()).get();
+        assertThat(fakePackage.getFailures()).hasSize(1);
+        assertThat(fakePackage.getFailures().get("id1").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
 
     // TODO(b/194207451) This test can be moved to CtsTestBase if customized logger is
@@ -73,12 +79,12 @@
     public void testLogger_searchStatsLogged_forEmptyFirstPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -96,10 +102,10 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
-        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
                 new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
                         logger).build()).get();
         assertThat(logger.mSearchStats).isNull();
@@ -113,7 +119,7 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(0);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -134,12 +140,12 @@
     public void testLogger_searchStatsLogged_forNonEmptyFirstPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -157,10 +163,10 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
-        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
                 new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
                         logger).build()).get();
         assertThat(logger.mSearchStats).isNull();
@@ -174,7 +180,7 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -195,12 +201,12 @@
     public void testLogger_searchStatsLogged_forEmptySecondPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -218,10 +224,10 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
-        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
                 new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
                         logger).build()).get();
         assertThat(logger.mSearchStats).isNull();
@@ -235,12 +241,12 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(2);
 
         // Get second(empty) page
         logger.mSearchStats = null;
-        page = searchResults.getNextPage().get();
+        page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(0);
 
         // Check searchStats has been set. We won't check all the fields here.
@@ -261,12 +267,12 @@
     public void testLogger_searchStatsLogged_forNonEmptySecondPage() throws Exception {
         SimpleTestLogger logger = new SimpleTestLogger();
         Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db2 = LocalStorage.createSearchSession(
+        AppSearchSession db2 = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2)
                         .setLogger(logger).build()).get();
 
         // Schema registration
-        db2.setSchema(
+        db2.setSchemaAsync(
                 new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
@@ -284,10 +290,10 @@
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(db2.put(
+        checkIsBatchResultSuccess(db2.putAsync(
                 new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1, inEmail2).build()));
 
-        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSession(
+        GlobalSearchSession globalSearchSession = LocalStorage.createGlobalSearchSessionAsync(
                 new LocalStorage.GlobalSearchContext.Builder(context).setLogger(
                         logger).build()).get();
         assertThat(logger.mSearchStats).isNull();
@@ -301,12 +307,12 @@
                 .build());
 
         // Get first page
-        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
 
         // Get second page
         logger.mSearchStats = null;
-        page = searchResults.getNextPage().get();
+        page = searchResults.getNextPageAsync().get();
         assertThat(page).hasSize(1);
 
         // Check searchStats has been set. We won't check all the fields here.
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
index 9d152ff..46aa5c4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionPlatformCtsTest.java
@@ -16,14 +16,11 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.cts.app;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import android.content.Context;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.platformstorage.PlatformStorage;
 import androidx.test.core.app.ApplicationProvider;
@@ -31,33 +28,19 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import org.junit.Ignore;
-import org.junit.Test;
-
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class GlobalSearchSessionPlatformCtsTest extends GlobalSearchSessionCtsTestBase {
     @Override
-    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+    protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createSearchSession(
+        return PlatformStorage.createSearchSessionAsync(
                 new PlatformStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
-    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSessionAsync() {
         Context context = ApplicationProvider.getApplicationContext();
-        return PlatformStorage.createGlobalSearchSession(
+        return PlatformStorage.createGlobalSearchSessionAsync(
                 new PlatformStorage.GlobalSearchContext.Builder(context).build());
     }
-
-    @Ignore("Unignore this test once these features are implemented in the platform backend.")
-    @Test
-    public void testFeaturesSupported() {
-        assertThat(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH))
-                .isEqualTo(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
-        assertThat(mDb1.getFeatures().isFeatureSupported(
-                Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER))
-                .isEqualTo(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
-    }
 }
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
index 372a090..c2b06d4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
@@ -74,12 +74,12 @@
         // 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(
+        AppSearchSession session = LocalStorage.createSearchSessionAsync(
                 new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "")
                         .build()
         ).get();
-        session.setSchema(new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build())
-            .get();
+        session.setSchemaAsync(
+                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)
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
index a965ef6..dd47914 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -220,6 +220,87 @@
     }
 
     @Test
+    public void testSetSchemaTypeVisibleForPermissions() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        // By default, the schema is displayed.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).build();
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
+
+        SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .addRequiredPermissionsForSchemaTypeVisibility("Schema",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForSchemaTypeVisibility("Schema",
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+
+        request = setSchemaRequestBuilder.build();
+
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly("Schema", ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest
+                                .READ_HOME_APP_SEARCH_DATA)));
+    }
+
+    @Test
+    public void testClearSchemaTypeVisibleForPermissions() {
+        SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+                .addSchemas(
+                        new AppSearchSchema.Builder("Schema1").build(),
+                        new AppSearchSchema.Builder("Schema2").build())
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        "Schema1",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        "Schema1",
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        "Schema2",
+                        ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE));
+
+        SetSchemaRequest request = setSchemaRequestBuilder.build();
+
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "Schema1", ImmutableSet.of(
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                        "Schema2", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+
+        // Clear the permissions in the builder
+        setSchemaRequestBuilder.clearRequiredPermissionsForSchemaTypeVisibility("Schema1");
+
+        // New object should be updated
+        assertThat(setSchemaRequestBuilder.build().getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "Schema2", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+
+        // Old object should remain unchanged
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "Schema1", ImmutableSet.of(
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                        "Schema2", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+    }
+
+    @Test
     public void testSchemaTypeVisibilityForPackage_visible() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
@@ -380,6 +461,29 @@
     }
 
     @Test
+    public void testSetDocumentClassVisibleForPermission() throws Exception {
+        // By default, the schema is displayed.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
+
+        SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+                .addDocumentClasses(Card.class)
+                .addRequiredPermissionsForDocumentClassVisibility(Card.class,
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForDocumentClassVisibility(Card.class,
+                ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+        request = setSchemaRequestBuilder.build();
+
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly("Card", ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest
+                                .READ_HOME_APP_SEARCH_DATA)));
+    }
+
+    @Test
     public void testSetDocumentClassVisibilityForPackage_visible() throws Exception {
         // By default, the schema is not visible.
         SetSchemaRequest request =
@@ -478,6 +582,58 @@
                 "King");
     }
 
+    @Test
+    public void testClearDocumentClassVisibleForPermissions() throws Exception {
+        SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder()
+                .addDocumentClasses(King.class, Queen.class)
+                .addRequiredPermissionsForDocumentClassVisibility(
+                        King.class,
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForDocumentClassVisibility(
+                        King.class,
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .addRequiredPermissionsForDocumentClassVisibility(
+                        Queen.class,
+                        ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE));
+
+        SetSchemaRequest request = setSchemaRequestBuilder.build();
+
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "King", ImmutableSet.of(
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                        "Queen", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+
+        // Clear the permissions in the builder
+        setSchemaRequestBuilder.clearRequiredPermissionsForDocumentClassVisibility(King.class);
+
+        // New object should be updated
+        assertThat(setSchemaRequestBuilder.build().getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "Queen", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+
+        // Old object should remain unchanged
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        "King", ImmutableSet.of(
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                                ImmutableSet.of(
+                                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                        "Queen", ImmutableSet.of(
+                                ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
+                        )
+            );
+    }
 // @exportToFramework:endStrip()
 
     @Test
@@ -498,4 +654,130 @@
         assertThat(exception).hasMessageThat().contains(
                 "Cannot set version to the request if schema is empty.");
     }
+
+    @Test
+    public void testRebuild() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+        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();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema1)
+                .setVersion(37)
+                .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+                .setSchemaTypeVisibilityForPackage(
+                        "Email1", /*visible=*/true, packageIdentifier1)
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA));
+
+        SetSchemaRequest original = builder.build();
+        SetSchemaRequest rebuild = builder.addSchemas(schema2)
+                .setVersion(42)
+                .setSchemaTypeDisplayedBySystem("Email2", /*displayed=*/false)
+                .setSchemaTypeVisibilityForPackage(
+                        "Email2", /*visible=*/true, packageIdentifier2)
+                .addRequiredPermissionsForSchemaTypeVisibility("Email2",
+                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                SetSchemaRequest.READ_EXTERNAL_STORAGE))
+                .addRequiredPermissionsForSchemaTypeVisibility("Email2",
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))
+                .build();
+
+        assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getVersion()).isEqualTo(37);
+        assertThat(original.getSchemasNotDisplayedBySystem()).containsExactly("Email1");
+        assertThat(original.getSchemasVisibleToPackages()).containsExactly(
+                "Email1", ImmutableSet.of(packageIdentifier1));
+        assertThat(original.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+                "Email1",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(rebuild.getVersion()).isEqualTo(42);
+        assertThat(rebuild.getSchemasNotDisplayedBySystem()).containsExactly("Email1", "Email2");
+        assertThat(rebuild.getSchemasVisibleToPackages()).containsExactly(
+                "Email1", ImmutableSet.of(packageIdentifier1),
+                "Email2", ImmutableSet.of(packageIdentifier2));
+        assertThat(rebuild.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+                "Email1",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)),
+                "Email2",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+    }
+
+    @Test
+    public void getAndModify() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+        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();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema1)
+                .setVersion(37)
+                .setSchemaTypeDisplayedBySystem("Email1", /*displayed=*/false)
+                .setSchemaTypeVisibilityForPackage(
+                        "Email1", /*visible=*/true, packageIdentifier1)
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR))
+                .addRequiredPermissionsForSchemaTypeVisibility("Email1",
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+
+        // get the visibility setting and modify the output object.
+        // skip getSchemasNotDisplayedBySystem since it returns an unmodifiable object.
+        request.getSchemasVisibleToPackages().put("Email2", ImmutableSet.of(packageIdentifier2));
+        request.getRequiredPermissionsForSchemaTypeVisibility().put("Email2",
+                ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CALENDAR)));
+
+        // verify we still get the original object.
+        assertThat(request.getSchemasVisibleToPackages()).containsExactly("Email1",
+                ImmutableSet.of(packageIdentifier1));
+        assertThat(request.getRequiredPermissionsForSchemaTypeVisibility()).containsExactly(
+                "Email1",
+                ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
new file mode 100644
index 0000000..06f1bd4
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.observer.DocumentChangeInfo;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class DocumentChangeInfoCtsTest {
+    @Test
+    public void testConstructor() {
+        DocumentChangeInfo DocumentChangeInfo = new DocumentChangeInfo(
+                "packageName",
+                "databaseName",
+                "namespace",
+                "SchemaName",
+                ImmutableSet.of("documentId1", "documentId2"));
+        assertThat(DocumentChangeInfo.getPackageName()).isEqualTo("packageName");
+        assertThat(DocumentChangeInfo.getDatabaseName()).isEqualTo("databaseName");
+        assertThat(DocumentChangeInfo.getNamespace()).isEqualTo("namespace");
+        assertThat(DocumentChangeInfo.getSchemaName()).isEqualTo("SchemaName");
+        assertThat(DocumentChangeInfo.getChangedDocumentIds())
+                .containsExactly("documentId1", "documentId2");
+    }
+
+    @Test
+    public void testEqualsAndHasCode() {
+        DocumentChangeInfo info1Copy1 = new DocumentChangeInfo(
+                "packageName",
+                "databaseName",
+                "namespace",
+                "SchemaName",
+                ImmutableSet.of("documentId1", "documentId2"));
+        DocumentChangeInfo info1Copy2 = new DocumentChangeInfo(
+                "packageName",
+                "databaseName",
+                "namespace",
+                "SchemaName",
+                ImmutableSet.of("documentId1", "documentId2"));
+        DocumentChangeInfo info2 = new DocumentChangeInfo(
+                "packageName",
+                "databaseName",
+                "namespace",
+                "SchemaName",
+                ImmutableSet.of("documentId3", "documentId2"));
+
+        assertThat(info1Copy1).isEqualTo(info1Copy2);
+        assertThat(info1Copy1.hashCode()).isEqualTo(info1Copy2.hashCode());
+        assertThat(info1Copy1).isNotEqualTo(info2);
+        assertThat(info1Copy1.hashCode()).isNotEqualTo(info2.hashCode());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java
new file mode 100644
index 0000000..ae321af
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/ObserverSpecCtsTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.observer.ObserverSpec;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class ObserverSpecCtsTest {
+    @Test
+    public void testFilterSchemas() {
+        ObserverSpec observerSpec = new ObserverSpec.Builder()
+                .addFilterSchemas("Schema1", "Schema2")
+                .addFilterSchemas(ImmutableSet.of("Schema3", "Schema4"))
+                .build();
+        assertThat(observerSpec.getFilterSchemas()).containsExactly(
+                "Schema1", "Schema2", "Schema3", "Schema4");
+    }
+
+// @exportToFramework:startStrip()
+
+    @Document
+    public static class King {
+        @Document.Namespace String mNamespace;
+        @Document.Id String mId;
+    }
+
+    @Document
+    public static class Queen {
+        @Document.Namespace String mNamespace;
+        @Document.Id String mId;
+    }
+
+    @Document
+    public static class Jack {
+        @Document.Namespace String mNamespace;
+        @Document.Id String mId;
+    }
+
+    @Document
+    public static class Ace {
+        @Document.Namespace String mNamespace;
+        @Document.Id String mId;
+    }
+
+    @Test
+    public void testFilterSchemas_documentClass() throws Exception {
+        ObserverSpec observerSpec = new ObserverSpec.Builder()
+                .addFilterSchemas("Schema1", "Schema2")
+                .addFilterDocumentClasses(King.class, Queen.class)
+                .addFilterSchemas(ImmutableSet.of("Schema3", "Schema4"))
+                .addFilterDocumentClasses(ImmutableSet.of(Jack.class, Ace.class))
+                .build();
+        assertThat(observerSpec.getFilterSchemas()).containsExactly(
+                "Schema1", "Schema2", "King", "Queen", "Schema3", "Schema4", "Jack", "Ace");
+    }
+
+// @exportToFramework:endStrip()
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java
new file mode 100644
index 0000000..3289177
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/SchemaChangeInfoCtsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.observer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.observer.SchemaChangeInfo;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+public class SchemaChangeInfoCtsTest {
+    @Test
+    public void testConstructor() {
+        SchemaChangeInfo schemaChangeInfo = new SchemaChangeInfo(
+                "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+        assertThat(schemaChangeInfo.getPackageName()).isEqualTo("packageName");
+        assertThat(schemaChangeInfo.getDatabaseName()).isEqualTo("databaseName");
+        assertThat(schemaChangeInfo.getChangedSchemaNames())
+                .containsExactly("schemaName1", "schemaName2");
+    }
+
+    @Test
+    public void testEqualsAndHasCode() {
+        SchemaChangeInfo info1Copy1 = new SchemaChangeInfo(
+                "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+        SchemaChangeInfo info1Copy2 = new SchemaChangeInfo(
+                "packageName", "databaseName", ImmutableSet.of("schemaName1", "schemaName2"));
+        SchemaChangeInfo info2 = new SchemaChangeInfo(
+                "packageName", "databaseName", ImmutableSet.of("schemaName3", "schemaName2"));
+
+        assertThat(info1Copy1).isEqualTo(info1Copy2);
+        assertThat(info1Copy1.hashCode()).isEqualTo(info1Copy2.hashCode());
+        assertThat(info1Copy1).isNotEqualTo(info2);
+        assertThat(info1Copy1.hashCode()).isNotEqualTo(info2.hashCode());
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
index 527127f..ef685c1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -220,7 +220,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
          * this attribute to {@code true}.
          */
         boolean required() default false;
@@ -256,7 +256,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
          * this attribute to {@code true}.
          */
         boolean required() default false;
@@ -280,7 +280,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
          * this attribute to {@code true}.
          */
         boolean required() default false;
@@ -307,7 +307,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
          * this attribute to {@code true}.
          */
         boolean required() default false;
@@ -331,7 +331,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before setting
          * this attribute to {@code true}.
          */
         boolean required() default false;
@@ -355,7 +355,7 @@
          * <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
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync 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 4b6ee7c..3cdf6ce 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -37,9 +37,9 @@
  * @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
+ * @see AppSearchSession#putAsync
+ * @see AppSearchSession#getByDocumentIdAsync
+ * @see AppSearchSession#removeAsync
  */
 public final class AppSearchBatchResult<KeyType, ValueType> {
     @NonNull private final Map<KeyType, ValueType> mSuccesses;
@@ -64,9 +64,9 @@
      * 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>Example: {@link AppSearchSession#getByDocumentIdAsync} 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}.
      */
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 84795f9..c3e8b5e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -44,7 +44,7 @@
  *
  * <p>The schema consists of type information, properties, and config (like tokenization type).
  *
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
  */
 public final class AppSearchSchema {
     private static final String SCHEMA_TYPE_FIELD = "schemaType";
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 d423da0..be67b84f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -32,7 +32,8 @@
  * 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()}.
+ * {@code LocalStorage.createSearchSessionAsync()} or
+ * {@code PlatformStorage.createSearchSessionAsync()}.
  *
  * <p>All implementations of this interface must be thread safe.
  *
@@ -44,42 +45,75 @@
      * Sets the schema that represents the organizational structure of data within the AppSearch
      * database.
      *
-     * <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>Upon creating an {@link AppSearchSession}, {@link #setSchemaAsync} 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 #setSchemaAsync} is handled
+     * efficiently as a no-op call.
      *
      * @param  request the schema to set or update the AppSearch database to.
      * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
      */
     @NonNull
-    ListenableFuture<SetSchemaResponse> setSchema(
-            @NonNull SetSchemaRequest request);
+    ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request);
 
     /**
-     * Retrieves the schema most recently successfully provided to {@link #setSchema}.
+     * @deprecated use {@link #setSchemaAsync}
+     * @param  request the schema to set or update the AppSearch database to.
+     * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
+     */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<SetSchemaResponse> setSchema(
+            @NonNull SetSchemaRequest request) {
+        return setSchemaAsync(request);
+    }
+
+    /**
+     * Retrieves the schema most recently successfully provided to {@link #setSchemaAsync}.
      *
      * @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<GetSchemaResponse> getSchema();
+    ListenableFuture<GetSchemaResponse> getSchemaAsync();
+
+    /**
+     * @deprecated use {@link #getSchemaAsync}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<GetSchemaResponse> getSchema() {
+        return getSchemaAsync();
+    }
 
     /**
      * Retrieves the set of all namespaces in the current database with at least one document.
      *
-     * @return The pending result of performing this operation.
-     */
+     * @return The pending result of performing this operation. */
     @NonNull
-    ListenableFuture<Set<String>> getNamespaces();
+    ListenableFuture<Set<String>> getNamespacesAsync();
+
+    /**
+     * @deprecated use {@link #getNamespacesAsync()}
+     *
+     * @return The pending result of performing this operation. */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<Set<String>> getNamespaces() {
+        return getNamespacesAsync();
+    }
 
     /**
      * Indexes documents into the {@link AppSearchSession} database.
      *
      * <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.
+     * {@link #setSchemaAsync} method.
      *
      * @param request containing documents to be indexed.
      * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
@@ -88,7 +122,24 @@
      * or a failed {@link AppSearchResult} otherwise.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> put(@NonNull PutDocumentsRequest request);
+    ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
+            @NonNull PutDocumentsRequest request);
+
+    /**
+     * @deprecated use {@link #putAsync}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<AppSearchBatchResult<String, Void>> put(
+            @NonNull PutDocumentsRequest request) {
+        return putAsync(request);
+    }
 
     /**
      * Gets {@link GenericDocument} objects by document IDs in a namespace from the
@@ -104,10 +155,29 @@
      * {@link AppSearchResult#RESULT_NOT_FOUND}.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
             @NonNull GetByDocumentIdRequest request);
 
     /**
+     * @deprecated use {@link #getByDocumentIdAsync}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request) {
+        return getByDocumentIdAsync(request);
+    }
+
+    /**
      * Retrieves documents from the open {@link AppSearchSession} that match a given query string
      * and type of search provided.
      *
@@ -164,7 +234,7 @@
      * 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}.
+     * {@link SearchResults#getNextPageAsync}.
      *
      * @param queryExpression query string to search.
      * @param searchSpec      spec for setting document filters, adding projection, setting term
@@ -179,9 +249,9 @@
      *
      * <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
+     * <p>For each call to {@link #reportUsageAsync}, 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.
@@ -191,14 +261,27 @@
      *     success.
      */
     @NonNull
-    ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request);
+    ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request);
+
+    /**
+     * @deprecated use {@link #reportUsageAsync}
+     *
+     * @param request The usage reporting request.
+     * @return The pending result of performing this operation which resolves to {@code null} on
+     *     success.
+     */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+        return reportUsageAsync(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}
+     * {@link #getByDocumentIdAsync}
      * calls.
      *
      * <p>Once the database crosses the document count or byte usage threshold, removed documents
@@ -213,10 +296,28 @@
      * {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+    ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
             @NonNull RemoveByDocumentIdRequest request);
 
     /**
+     * @deprecated use {@link #removeAsync}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request) {
+        return removeAsync(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#addFilterNamespaces} and
@@ -234,7 +335,24 @@
      * @return The pending result of performing this operation.
      */
     @NonNull
-    ListenableFuture<Void> remove(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+    ListenableFuture<Void> removeAsync(@NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec);
+
+    /**
+     * @deprecated use {@link #removeAsync}
+     *
+     * @param queryExpression Query String to search.
+     * @param searchSpec      Spec containing schemaTypes, namespaces and query expression
+     *                        indicates how document will be removed. All specific about how to
+     *                        scoring, ordering, snippeting and resulting will be ignored.
+     * @return The pending result of performing this operation.
+     */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<Void> remove(@NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        return removeAsync(queryExpression, searchSpec);
+    }
 
     /**
      * Gets the storage info for this {@link AppSearchSession} database.
@@ -245,7 +363,18 @@
      * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
      */
     @NonNull
-    ListenableFuture<StorageInfo> getStorageInfo();
+    ListenableFuture<StorageInfo> getStorageInfoAsync();
+
+    /**
+     * @deprecated use {@link #getStorageInfoAsync()}
+     *
+     * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
+     */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<StorageInfo> getStorageInfo() {
+        return getStorageInfoAsync();
+    }
 
     /**
      * Flush all schema and document updates, additions, and deletes to disk if possible.
@@ -259,7 +388,21 @@
      * save to disk.
      */
     @NonNull
-    ListenableFuture<Void> requestFlush();
+    ListenableFuture<Void> requestFlushAsync();
+
+    /**
+     * @deprecated use {@link #requestFlushAsync()}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<Void> requestFlush() {
+        return requestFlushAsync();
+    }
 
     /**
      * Returns the {@link Features} to check for the availability of certain features
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index 26ab787..dc530ee 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -24,9 +24,9 @@
  * <p>Features do not depend on any runtime state, and features will never be removed. Once
  * {@link #isFeatureSupported} returns {@code true} for a certain feature, it is safe to assume that
  * the feature will be available forever on that AppSearch storage implementation, at that
- * Android API level, on that device form factor.
- * <!--@exportToFramework:hide-->
+ * Android API level, on that device.
  */
+// @exportToFramework:copyToPath(testing/testutils/src/android/app/appsearch/testutil/external/Features.java)
 public interface Features {
 
     /**
@@ -38,10 +38,35 @@
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link GlobalSearchSession#addObserver} and
-     * {@link GlobalSearchSession#removeObserver}.
+     * {@link GlobalSearchSession#registerObserverCallback} and
+     * {@link GlobalSearchSession#unregisterObserverCallback}.
      */
-    String GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER = "GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER";
+    String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK =
+            "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link GlobalSearchSession#getSchemaAsync}.
+     */
+    String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link GlobalSearchSession#getByDocumentIdAsync}.
+     */
+    String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},
+     * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},
+     * {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()},
+     * {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()},
+     * {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} and
+     * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility}
+     */
+    String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
 
     /**
      * Returns whether a feature is supported at run-time. Feature support depends on the
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 f1577f7..db06bda 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -51,8 +51,8 @@
  * <p>Documents are constructed by using the {@link GenericDocument.Builder}.
  * -->
  *
- * @see AppSearchSession#put
- * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#putAsync
+ * @see AppSearchSession#getByDocumentIdAsync
  * @see AppSearchSession#search
  */
 public class GenericDocument {
@@ -81,7 +81,7 @@
      *
      * <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}.
+     * than {@link AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}.
      */
     public static int getMaxIndexedProperties() {
         return MAX_INDEXED_PROPERTIES;
@@ -205,7 +205,7 @@
      * 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 or {@link AppSearchSession#remove} is called.
+     * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called.
      */
     public long getTtlMillis() {
         return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
@@ -1103,12 +1103,12 @@
          * @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
+         *                   {@link AppSearchSession#setSchemaAsync} prior
          *                   to inserting a document of this {@code schemaType} into the
          *                   AppSearch index using
-         *                   {@link AppSearchSession#put}.
+         *                   {@link AppSearchSession#putAsync}.
          *                   Otherwise, the document will be rejected by
-         *                   {@link AppSearchSession#put} with result code
+         *                   {@link AppSearchSession#putAsync} with result code
          *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
          */
         @SuppressWarnings("unchecked")
@@ -1177,7 +1177,7 @@
          * 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}.
+         * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}.
          * <!--@exportToFramework:hide-->
          */
         @NonNull
@@ -1238,7 +1238,7 @@
          * {@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 or {@link AppSearchSession#remove} is
+         * auto-deleted until the app is uninstalled or {@link AppSearchSession#removeAsync} is
          * called.
          *
          * @param ttlMillis a non-negative duration in milliseconds.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
index d9bfc0d..b2ebf3d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -34,7 +34,7 @@
  * Encapsulates a request to retrieve documents by namespace and IDs from the
  * {@link AppSearchSession} database.
  *
- * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#getByDocumentIdAsync
  */
 public final class GetByDocumentIdRequest {
     /**
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
index f169099..fec585f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -16,21 +16,62 @@
 
 package androidx.appsearch.app;
 
+import android.annotation.SuppressLint;
 import android.os.Bundle;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
-/** The response class of {@link AppSearchSession#getSchema} */
+/** The response class of {@link AppSearchSession#getSchemaAsync} */
 public final class GetSchemaResponse {
     private static final String VERSION_FIELD = "version";
     private static final String SCHEMAS_FIELD = "schemas";
+    private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
+            "schemasNotDisplayedBySystem";
+    private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
+    private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD =
+            "schemasVisibleToPermissions";
+    private static final String ALL_REQUIRED_PERMISSION_FIELD =
+            "allRequiredPermission";
+    /**
+     * This Set contains all schemas that are not displayed by the system. All values in the set are
+     * prefixed with the package-database prefix. We do lazy fetch, the object will be created
+     * when the user first time fetch it.
+     */
+    @Nullable
+    private Set<String> mSchemasNotDisplayedBySystem;
+    /**
+     * This map contains all schemas and {@link PackageIdentifier} that has access to the schema.
+     * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the
+     * object will be created when the user first time fetch it.
+     */
+    @Nullable
+    private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+
+    /**
+     * This map contains all schemas and Android Permissions combinations that are required to
+     * access the schema. All keys in the map are prefixed with the package-database prefix. We
+     * do lazy fetch, the object will be created when the user first time fetch it.
+     * The Map is constructed in ANY-ALL cases. The querier could read the {@link GenericDocument}
+     * objects under the {@code schemaType} if they holds ALL required permissions of ANY
+     * combinations.
+     * The value set represents
+     * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
+     */
+    @Nullable
+    private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
 
     private final Bundle mBundle;
 
@@ -60,7 +101,7 @@
 
     /**
      * Return the schemas most recently successfully provided to
-     * {@link AppSearchSession#setSchema}.
+     * {@link AppSearchSession#setSchemaAsync}.
      *
      * <p>It is inefficient to call this method repeatedly.
      */
@@ -75,12 +116,161 @@
         return schemas;
     }
 
+    /**
+     * Returns all the schema types that are opted out of being displayed and visible on any
+     * system UI surface.
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    @NonNull
+    public Set<String> getSchemaTypesNotDisplayedBySystem() {
+        checkGetVisibilitySettingSupported();
+        if (mSchemasNotDisplayedBySystem == null) {
+            List<String> schemasNotDisplayedBySystemList =
+                    mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);
+            mSchemasNotDisplayedBySystem =
+                    Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));
+        }
+        return mSchemasNotDisplayedBySystem;
+    }
+
+    /**
+     * Returns a mapping of schema types to the set of packages that have access
+     * to that schema type.
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    @NonNull
+    @SuppressWarnings("deprecation")
+    public Map<String, Set<PackageIdentifier>> getSchemaTypesVisibleToPackages() {
+        checkGetVisibilitySettingSupported();
+        if (mSchemasVisibleToPackages == null) {
+            Bundle schemaVisibleToPackagesBundle =
+                    mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD);
+            Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+            for (String key : schemaVisibleToPackagesBundle.keySet()) {
+                List<Bundle> PackageIdentifierBundles = schemaVisibleToPackagesBundle
+                        .getParcelableArrayList(key);
+                Set<PackageIdentifier> packageIdentifiers =
+                        new ArraySet<>(PackageIdentifierBundles.size());
+                for (int i = 0; i < PackageIdentifierBundles.size(); i++) {
+                    packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));
+                }
+                copy.put(key, packageIdentifiers);
+            }
+            mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
+        }
+        return mSchemasVisibleToPackages;
+    }
+
+    /**
+     * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+     * combinations that querier must hold to access that schema type.
+     *
+     * <p> The querier could read the {@link GenericDocument} objects under the {@code schemaType}
+     * if they holds ALL required permissions of ANY of the individual value sets.
+     *
+     * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+     * { PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+     * <ul>
+     *     <li>A querier holds both PermissionA and PermissionB has access.</li>
+     *     <li>A querier holds both PermissionC and PermissionD has access.</li>
+     *     <li>A querier holds only PermissionE has access.</li>
+     *     <li>A querier holds both PermissionA and PermissionE has access.</li>
+     *     <li>A querier holds only PermissionA doesn't have access.</li>
+     *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+     * </ul>
+     *
+     * @return The map contains schema type and all combinations of required permission for querier
+     *         to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS},
+     *         {@link SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS},
+     *         {@link SetSchemaRequest#READ_EXTERNAL_STORAGE},
+     *         {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
+     *         {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    @SetSchemaRequest.AppSearchSupportedPermission
+    @NonNull
+    @SuppressWarnings("deprecation")
+    public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+        checkGetVisibilitySettingSupported();
+        if (mSchemasVisibleToPermissions == null) {
+            Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
+            Bundle schemaVisibleToPermissionBundle =
+                    mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD);
+            for (String key : schemaVisibleToPermissionBundle.keySet()) {
+                ArrayList<Bundle> allRequiredPermissionsBundle =
+                        schemaVisibleToPermissionBundle.getParcelableArrayList(key);
+                Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
+                if (allRequiredPermissionsBundle != null) {
+                    // This should never be null
+                    for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {
+                        visibleToPermissions.add(new ArraySet<>(allRequiredPermissionsBundle.get(i)
+                                .getIntegerArrayList(ALL_REQUIRED_PERMISSION_FIELD)));
+                    }
+                }
+                copy.put(key, visibleToPermissions);
+            }
+            mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
+        }
+        return mSchemasVisibleToPermissions;
+    }
+
+    private void checkGetVisibilitySettingSupported() {
+        if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
+            throw new UnsupportedOperationException("Get visibility setting is not supported with"
+                    + " this backend/Android API level combination.");
+        }
+    }
+
     /** Builder for {@link GetSchemaResponse} objects. */
     public static final class Builder {
         private int mVersion = 0;
         private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+        /**
+         * Creates the object when we actually set them. If we never set visibility settings, we
+         * should throw {@link UnsupportedOperationException} in the visibility getters.
+         */
+        @Nullable
+        private ArrayList<String> mSchemasNotDisplayedBySystem;
+        private Bundle mSchemasVisibleToPackages;
+        private Bundle mSchemasVisibleToPermissions;
         private boolean mBuilt = false;
 
+        /** Create a {@link Builder} object} */
+        public Builder() {
+            this(/*getVisibilitySettingSupported=*/true);
+        }
+
+        /**
+         * Create a {@link Builder} object}.
+         *
+         * <p>This constructor should only be used in Android API below than T.
+         *
+         * @param getVisibilitySettingSupported whether supported
+         * {@link Features#ADD_PERMISSIONS_AND_GET_VISIBILITY} by this
+         *                                      backend/Android API level.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(boolean getVisibilitySettingSupported) {
+            if (getVisibilitySettingSupported) {
+                mSchemasNotDisplayedBySystem = new ArrayList<>();
+                mSchemasVisibleToPackages = new Bundle();
+                mSchemasVisibleToPermissions = new Bundle();
+            }
+        }
+
         /**
          * Sets the database overall schema version.
          *
@@ -102,12 +292,130 @@
             return this;
         }
 
+        /**
+         * Sets whether or not documents from the provided {@code schemaType} will be displayed
+         * and visible on any system UI surface.
+         *
+         * @param schemaType The name of an {@link AppSearchSchema} within the same
+         *                   {@link GetSchemaResponse}, which won't be displayed by system.
+         */
+        // Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            resetIfBuilt();
+            if (mSchemasNotDisplayedBySystem == null) {
+                mSchemasNotDisplayedBySystem = new ArrayList<>();
+            }
+            mSchemasNotDisplayedBySystem.add(schemaType);
+            return this;
+        }
+
+        /**
+         * 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.
+         *
+         * @param schemaType               The schema type to set visibility on.
+         * @param packageIdentifiers       Represents the package that has access to the given
+         *                                 schema type.
+         */
+        // Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setSchemaTypeVisibleToPackages(
+                @NonNull String schemaType,
+                @NonNull Set<PackageIdentifier> packageIdentifiers) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(packageIdentifiers);
+            resetIfBuilt();
+            ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
+            for (PackageIdentifier packageIdentifier : packageIdentifiers) {
+                bundles.add(packageIdentifier.getBundle());
+            }
+            mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
+            return this;
+        }
+
+        /**
+         * Sets a set of required {@link android.Manifest.permission} combinations to the given
+         * schema type.
+         *
+         * <p> The querier could read the {@link GenericDocument} objects under the
+         * {@code schemaType} if they holds ALL required permissions of ANY of the individual value
+         * sets.
+         *
+         * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+         * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+         * <ul>
+         *     <li>A querier holds both PermissionA and PermissionB has access.</li>
+         *     <li>A querier holds both PermissionC and PermissionD has access.</li>
+         *     <li>A querier holds only PermissionE has access.</li>
+         *     <li>A querier holds both PermissionA and PermissionE has access.</li>
+         *     <li>A querier holds only PermissionA doesn't have access.</li>
+         *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+         * </ul>
+         *
+         * @see android.Manifest.permission#READ_SMS
+         * @see android.Manifest.permission#READ_CALENDAR
+         * @see android.Manifest.permission#READ_CONTACTS
+         * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+         * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+         * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+         *
+         * @param schemaType             The schema type to set visibility on.
+         * @param visibleToPermissions   The Android permissions that will be required to access
+         *                               the given schema.
+         */
+        // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setRequiredPermissionsForSchemaTypeVisibility(
+                @NonNull String schemaType,
+                @SetSchemaRequest.AppSearchSupportedPermission @NonNull
+                        Set<Set<Integer>> visibleToPermissions) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(visibleToPermissions);
+            resetIfBuilt();
+            ArrayList<Bundle> visibleToPermissionsBundle = new ArrayList<>();
+            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+                for (int permission : allRequiredPermissions) {
+                    Preconditions.checkArgumentInRange(permission, SetSchemaRequest.READ_SMS,
+                            SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+                }
+                Bundle allRequiredPermissionsBundle = new Bundle();
+                allRequiredPermissionsBundle.putIntegerArrayList(
+                        ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));
+                visibleToPermissionsBundle.add(allRequiredPermissionsBundle);
+            }
+            mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,
+                    visibleToPermissionsBundle);
+            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);
+            if (mSchemasNotDisplayedBySystem != null) {
+                // Only save the visibility fields if it was actually set.
+                bundle.putStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD,
+                        mSchemasNotDisplayedBySystem);
+                bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);
+                bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);
+            }
             mBuilt = true;
             return new GetSchemaResponse(bundle);
         }
@@ -115,6 +423,16 @@
         private void resetIfBuilt() {
             if (mBuilt) {
                 mSchemaBundles = new ArrayList<>(mSchemaBundles);
+                if (mSchemasNotDisplayedBySystem != null) {
+                    // Only reset the visibility fields if it was actually set.
+                    mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);
+                    Bundle copyVisibleToPackages = new Bundle();
+                    copyVisibleToPackages.putAll(mSchemasVisibleToPackages);
+                    mSchemasVisibleToPackages = copyVisibleToPackages;
+                    Bundle copyVisibleToPermissions = new Bundle();
+                    copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);
+                    mSchemasVisibleToPermissions = copyVisibleToPermissions;
+                }
                 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 6784803..4ce54b4 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -16,9 +16,12 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresFeature;
-import androidx.appsearch.observer.AppSearchObserverCallback;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -36,6 +39,31 @@
  */
 public interface GlobalSearchSession extends Closeable {
     /**
+     * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
+     * database name and identified by the namespace and ids in the request, from the
+     * {@link GlobalSearchSession} database. When a call is successful, the result will be
+     * returned in the successes section of the {@link AppSearchBatchResult} object in the callback.
+     * If the package doesn't exist, database doesn't exist, or if the calling package doesn't have
+     * access, these failures will be reflected as {@link AppSearchResult} objects with a
+     * RESULT_NOT_FOUND status code in the failures section of the {@link AppSearchBatchResult}
+     * object.
+     *
+     * @param packageName the name of the package to get from
+     * @param databaseName the name of the database to get from
+     * @param request a request containing a namespace and IDs of the documents to retrieve.
+     */
+    @NonNull
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)
+    // @exportToFramework:endStrip()
+    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull GetByDocumentIdRequest request);
+
+    /**
      * Retrieves documents from all AppSearch databases that the querying application has access to.
      *
      * <p>Applications can be granted access to documents by specifying
@@ -51,7 +79,7 @@
      * forming a query string.
      *
      * <p>This method is lightweight. The heavy work will be done in
-     * {@link SearchResults#getNextPage}.
+     * {@link SearchResults#getNextPageAsync}.
      *
      * @param queryExpression query string to search.
      * @param searchSpec      spec for setting document filters, adding projection, setting term
@@ -64,11 +92,11 @@
     /**
      * 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>See {@link AppSearchSession#reportUsageAsync} 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 AppSearchSession#reportUsageAsync} and may be accessed using the constants
      * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
      * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
      *
@@ -79,7 +107,70 @@
      *     is not part of the system.
      */
     @NonNull
-    ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request);
+    ListenableFuture<Void> reportSystemUsageAsync(@NonNull ReportSystemUsageRequest request);
+
+    /**
+     * @deprecated use {@link #reportSystemUsageAsync}
+     *
+     * @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
+    @Deprecated
+    default ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+        return reportSystemUsageAsync(request);
+    }
+
+    /**
+     * Retrieves the collection of schemas most recently successfully provided to
+     * {@link AppSearchSession#setSchemaAsync} for any types belonging to the requested package and
+     * database that the caller has been granted access to.
+     *
+     * <p> If the requested package/database combination does not exist or the caller has not been
+     * granted access to it, then an empty GetSchemaResponse will be returned.
+     *
+     *
+     * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+     * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+     * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+     * access to or an empty GetSchemaResponse if the request package and database does not
+     * exist, has not set a schema or contains no schemas that are accessible to the caller.
+     */
+    // This call hits disk; async API prevents us from treating these calls as properties.
+    @SuppressLint("KotlinPropertyAccess")
+    @NonNull
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)
+    // @exportToFramework:endStrip()
+    ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+            @NonNull String databaseName);
+
+    /**
+     * @deprecated use {@link #getSchemaAsync}.
+     *
+     * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+     * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+     * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+     * access to or an empty GetSchemaResponse if the request package and database does not
+     * exist, has not set a schema or contains no schemas that are accessible to the caller.
+     */
+    @SuppressLint("KotlinPropertyAccess")
+    @NonNull
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)
+    // @exportToFramework:endStrip()
+    @Deprecated
+    default ListenableFuture<GetSchemaResponse> getSchema(@NonNull String packageName,
+            @NonNull String databaseName) {
+        return getSchemaAsync(packageName, databaseName);
+    }
 
     /**
      * Returns the {@link Features} to check for the availability of certain features
@@ -89,63 +180,75 @@
     Features getFeatures();
 
     /**
-     * Adds an {@link AppSearchObserverCallback} to monitor changes within the
-     * databases owned by {@code observedPackage} if they match the given
+     * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
+     * {@code targetPackageName} if they match the given
      * {@link androidx.appsearch.observer.ObserverSpec}.
      *
-     * <p>If the data owned by {@code observedPackage} is not visible to you, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} changes its schema visibility settings.
+     * <p>The observer callback is only triggered for data that changes after it is registered. No
+     * notification about existing data is sent as a result of registering an observer. To find out
+     * about existing data, you must use the {@link GlobalSearchSession#search} API.
      *
-     * <p>If no package matching {@code observedPackage} exists on the system, the registration call
-     * will succeed but no notifications will be dispatched. Notifications could start flowing later
-     * if {@code observedPackage} is installed and starts indexing data.
+     * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} changes its schema visibility settings.
+     *
+     * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+     * call will succeed but no notifications will be dispatched. Notifications could start flowing
+     * later if {@code targetPackageName} is installed and starts indexing data.
      *
      * <p>This feature may not be available in all implementations. Check
-     * {@link Features#GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER} before calling this method.
+     * {@link Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
      *
-     * @param observedPackage Package whose changes to monitor
+     * @param targetPackageName Package whose changes to monitor
      * @param spec            Specification of what types of changes to listen for
      * @param executor        Executor on which to call the {@code observer} callback methods.
      * @param observer        Callback to trigger when a schema or document changes
+     * @throws AppSearchException            if an error occurs trying to register the observer
      * @throws UnsupportedOperationException if this feature is not available on this
      *                                       AppSearch implementation.
      */
     // @exportToFramework:startStrip()
     @RequiresFeature(
             enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
-            name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+            name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
     // @exportToFramework:endStrip()
-    void addObserver(
-            @NonNull String observedPackage,
+    void registerObserverCallback(
+            @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull AppSearchObserverCallback observer);
+            @NonNull ObserverCallback observer) throws AppSearchException;
 
     /**
-     * Removes previously registered {@link AppSearchObserverCallback} instances from the system.
+     * Removes previously registered {@link ObserverCallback} instances from the system.
      *
-     * <p>All instances of {@link AppSearchObserverCallback} which are equal to the provided
-     * callback using {@link AppSearchObserverCallback#equals} will be removed.
+     * <p>All instances of {@link ObserverCallback} which are registered to observe
+     * {@code targetPackageName} and compare equal to the provided callback using the provided
+     * argument's {@link ObserverCallback#equals} will be removed.
      *
      * <p>If no matching observers have been registered, this method has no effect. If multiple
      * matching observers have been registered, all will be removed.
      *
      * <p>This feature may not be available in all implementations. Check
-     * {@link Features#GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER} before calling this method.
+     * {@link Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
      *
-     * @param observedPackage Package in which the observers to be removed are registered
-     * @param observer        Callback to unregister
+     * @param targetPackageName Package which the observers to be removed are listening to.
+     * @param observer          Callback to unregister.
+     * @throws AppSearchException            if an error occurs trying to remove the observer, such
+     *                                       as a failure to communicate with the system service
+     *                                       in the platform backend. Note that no
+     *                                       error will be thrown if the provided observer
+     *                                       doesn't match any registered observer.
      * @throws UnsupportedOperationException if this feature is not available on this
      *                                       AppSearch implementation.
      */
     // @exportToFramework:startStrip()
     @RequiresFeature(
             enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
-            name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+            name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
     // @exportToFramework:endStrip()
-    void removeObserver(
-            @NonNull String observedPackage, @NonNull AppSearchObserverCallback observer);
+    void unregisterObserverCallback(
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+            throws AppSearchException;
 
     /** Closes the {@link GlobalSearchSession}. */
     @Override
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
index b47735b..e6a7b2c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
@@ -51,11 +51,11 @@
      * 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
+     * {@link AppSearchSession#setSchemaAsync}, 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}.
+     * {@link AppSearchSession#setSchemaAsync}.
      *
      * @param currentVersion The current version of the document's schema.
      * @param finalVersion  The final version that documents need to be migrated to.
@@ -74,7 +74,7 @@
      * 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
+     * {@link AppSearchSession#setSchemaAsync}, 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.
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 4d4a000..dff69b2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -37,7 +37,7 @@
  * {@link GenericDocument}.
  * <!--@exportToFramework:else()-->
  *
- * @see AppSearchSession#put
+ * @see AppSearchSession#putAsync
  */
 public final class PutDocumentsRequest {
     private final List<GenericDocument> mDocuments;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
index addb96a..38be17a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -29,7 +29,7 @@
  * Encapsulates a request to remove documents by namespace and IDs from the
  * {@link AppSearchSession} database.
  *
- * @see AppSearchSession#remove
+ * @see AppSearchSession#removeAsync
  */
 public final class RemoveByDocumentIdRequest {
     private final String mNamespace;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
index db26931..12422eb 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -23,9 +23,9 @@
  * 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}.
+ * {@link AppSearchSession#reportUsageAsync}.
  *
- * <p>See {@link GlobalSearchSession#reportSystemUsage} for a detailed description of usage
+ * <p>See {@link GlobalSearchSession#reportSystemUsageAsync} for a detailed description of usage
  * reporting.
  */
 public final class ReportSystemUsageRequest {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
index 25edaf4..14b70c7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -22,9 +22,9 @@
 /**
  * A request to report usage of a document.
  *
- * <p>See {@link AppSearchSession#reportUsage} for a detailed description of usage reporting.
+ * <p>See {@link AppSearchSession#reportUsageAsync} for a detailed description of usage reporting.
  *
- * @see AppSearchSession#reportUsage
+ * @see AppSearchSession#reportUsageAsync
  */
 public final class ReportUsageRequest {
     private final String mNamespace;
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 7350991..17ed126 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -268,52 +268,68 @@
     }
 
     /**
-     * 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.
+     * This class represents match objects for any Snippets that might be present in
+     * {@link SearchResults} from a query. Using this class, the user can get:
+     * <ul>
+     *     <li>the full text - all of the text in that String property</li>
+     *     <li>the exact term match - the 'term' (full word) that matched the query</li>
+     *     <li>the subterm match - the portion of the matched term that appears in the query</li>
+     *     <li>a suggested text snippet - a portion of the full text surrounding the exact term
+     *     match, set to term boundaries. The size of the snippet is specified in
+     *     {@link SearchSpec.Builder#setMaxSnippetSize}</li>
+     * </ul>
+     * for each match in the document.
      *
      * <p>Class Example 1:
-     * A document contains following text in property subject:
-     * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
+     * <p>A document contains the following text in property "subject":
+     * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar."
      *
-     * <p>If the queryExpression is "foo".
-     *
-     * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
-     * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
-     * nonsense word that’s used a lot is bar."
-     * <p>{@link MatchInfo#getExactMatchRange()} returns [29, 32]
-     * <p>{@link MatchInfo#getExactMatch()} returns "foo"
-     * <p>{@link MatchInfo#getSubmatchRange()} returns [29, 32]
-     * <p>{@link MatchInfo#getSubmatch()} returns "foo"
-     * <p>{@link MatchInfo#getSnippetRange()} returns [26, 33]
-     * <p>{@link MatchInfo#getSnippet()} returns "is foo."
+     * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize}  is 10,
+     * <ul>
+     *      <li>{@link MatchInfo#getPropertyPath()} returns "subject"</li>
+     *      <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
+     * nonsense word that’s used a lot is bar."</li>
+     *      <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32]</li>
+     *      <li>{@link MatchInfo#getExactMatch()} returns "foo"</li>
+     *      <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32]</li>
+     *      <li>{@link MatchInfo#getSubmatch()} returns "foo"</li>
+     *      <li>{@link MatchInfo#getSnippetRange()} returns [26, 33]</li>
+     *      <li>{@link MatchInfo#getSnippet()} returns "is foo."</li>
+     * </ul>
      * <p>
      * <p>Class Example 2:
-     * A document contains a property name sender which contains 2 property names name and email, so
-     * we will have 2 property paths: {@code sender.name} and {@code sender.email}.
-     * <p>Let {@code sender.name = "Test Name Jr."} and
-     * {@code sender.email = "TestNameJr@gmail.com"}
+     * <p>A document contains one property named "subject" and one property named "sender" which
+     * contains a "name" property.
      *
-     * <p>If the queryExpression is "Test". We will have 2 matches.
+     * In this case, we will have 2 property paths: {@code sender.name} and {@code subject}.
+     * <p>Let {@code sender.name = "Test Name Jr."} and
+     * {@code subject = "Testing 1 2 3"}
+     *
+     * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and
+     * {@link SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches:
      *
      * <p> Match-1
-     * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
-     * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
-     * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 4]
-     * <p>{@link MatchInfo#getExactMatch()} returns "Test"
-     * <p>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
-     * <p>{@link MatchInfo#getSubmatch()} returns "Test"
-     * <p>{@link MatchInfo#getSnippetRange()} returns [0, 9]
-     * <p>{@link MatchInfo#getSnippet()} returns "Test Name"
+     * <ul>
+     *      <li>{@link MatchInfo#getPropertyPath()} returns "sender.name"</li>
+     *      <li>{@link MatchInfo#getFullText()} returns "Test Name Jr."</li>
+     *      <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4]</li>
+     *      <li>{@link MatchInfo#getExactMatch()} returns "Test"</li>
+     *      <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
+     *      <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
+     *      <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
+     *      <li>{@link MatchInfo#getSnippet()} returns "Test Name"</li>
+     * </ul>
      * <p> Match-2
-     * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
-     * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
-     * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 20]
-     * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
-     * <p>{@link MatchInfo#getSubmatchRange()} returns [0, 4]
-     * <p>{@link MatchInfo#getSubmatch()} returns "Test"
-     * <p>{@link MatchInfo#getSnippetRange()} returns [0, 20]
-     * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
+     * <ul>
+     *      <li>{@link MatchInfo#getPropertyPath()} returns "subject"</li>
+     *      <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3"</li>
+     *      <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7]</li>
+     *      <li>{@link MatchInfo#getExactMatch()} returns "Testing"</li>
+     *      <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
+     *      <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
+     *      <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
+     *      <li>{@link MatchInfo#getSnippet()} returns "Testing 1"</li>
+     * </ul>
      */
     public static final class MatchInfo {
         /** The path of the matching snippet property. */
@@ -378,8 +394,10 @@
 
         /**
          * Gets the full text corresponding to the given entry.
-         * <p>For class example this returns "A commonly used fake word is foo. Another nonsense
+         * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense
          * word that's used a lot is bar."
+         * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name Jr." and,
+         * for the second {@link MatchInfo}, this returns "Testing 1 2 3".
          */
         @NonNull
         public String getFullText() {
@@ -396,7 +414,7 @@
          * Gets the {@link MatchRange} of the exact term of the given entry that matched the query.
          * <p>Class example 1: this returns [29, 32].
          * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the
-         * second {@link MatchInfo}, this returns [0, 20].
+         * second {@link MatchInfo}, this returns [0, 7].
          */
         @NonNull
         public MatchRange getExactMatchRange() {
@@ -412,7 +430,7 @@
          * Gets the exact term of the given entry that matched the query.
          * <p>Class example 1: this returns "foo".
          * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the
-         * second {@link MatchInfo}, this returns "TestNameJr@gmail.com".
+         * second {@link MatchInfo}, this returns "Testing".
          */
         @NonNull
         public CharSequence getExactMatch() {
@@ -481,7 +499,7 @@
          * {@link SearchSpec.Builder#setMaxSnippetSize}.
          * <p>Class example 1: this returns [29, 41].
          * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 9] and, for the
-         * second {@link MatchInfo}, this returns [0, 20].
+         * second {@link MatchInfo}, this returns [0, 13].
          */
         @NonNull
         public MatchRange getSnippetRange() {
@@ -501,7 +519,7 @@
          * the matched token with content on either side clipped to token boundaries.
          * <p>Class example 1: this returns "foo. Another".
          * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name" and, for
-         * the second {@link MatchInfo}, this returns "TestNameJr@gmail.com".
+         * the second {@link MatchInfo}, this returns "Testing 1 2 3".
          */
         @NonNull
         public CharSequence getSnippet() {
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 6bf301f..23b6b5d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
@@ -30,7 +30,7 @@
  * objects, referred to as a "page", limited by the size configured by
  * {@link SearchSpec.Builder#setResultCountPerPage}.
  *
- * <p>To fetch a page of results, call {@link #getNextPage()}.
+ * <p>To fetch a page of results, call {@link #getNextPageAsync()}.
  *
  * <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
  * results are fetched.
@@ -50,7 +50,18 @@
      * objects.
      */
     @NonNull
-    ListenableFuture<List<SearchResult>> getNextPage();
+    ListenableFuture<List<SearchResult>> getNextPageAsync();
+
+    /**
+     * @deprecated use {@link #getNextPageAsync}.
+     * @return a {@link ListenableFuture} which resolves to a list of {@link SearchResult}
+     * objects.
+     */
+    @NonNull
+    @Deprecated
+    default ListenableFuture<List<SearchResult>> getNextPage() {
+        return getNextPageAsync();
+    }
 
     @Override
     void close();
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 e09eec9..94fc7d6 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -18,14 +18,18 @@
 
 import android.annotation.SuppressLint;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -76,13 +80,105 @@
  *         will be set into both {@link SetSchemaResponse#getIncompatibleTypes()} and
  *         {@link SetSchemaResponse#getMigratedTypes()}. See the migration section below.
  * </ul>
- * @see AppSearchSession#setSchema
+ * @see AppSearchSession#setSchemaAsync
  * @see Migrator
  */
 public final class SetSchemaRequest {
+
+    /**
+     * List of Android Permission are supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     *
+     * @see android.Manifest.permission
+     * @hide
+     */
+    @IntDef(value = {
+            READ_SMS,
+            READ_CALENDAR,
+            READ_CONTACTS,
+            READ_EXTERNAL_STORAGE,
+            READ_HOME_APP_SEARCH_DATA,
+            READ_ASSISTANT_APP_SEARCH_DATA,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @interface AppSearchSupportedPermission {}
+
+    /**
+     * The {@link android.Manifest.permission#READ_SMS} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_SMS = 1;
+
+    /**
+     * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_CALENDAR = 2;
+
+    /**
+     * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_CONTACTS = 3;
+
+    /**
+     * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_EXTERNAL_STORAGE = 4;
+
+    /**
+     * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_HOME_APP_SEARCH_DATA = 5;
+
+    /**
+     * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+     */
+    // @exportToFramework:startStrip()
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    // @exportToFramework:endStrip()
+    public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
+
     private final Set<AppSearchSchema> mSchemas;
     private final Set<String> mSchemasNotDisplayedBySystem;
     private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+    private final Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
     private final Map<String, Migrator> mMigrators;
     private final boolean mForceOverride;
     private final int mVersion;
@@ -90,12 +186,14 @@
     SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
             @NonNull Set<String> schemasNotDisplayedBySystem,
             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
+            @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
             @NonNull Map<String, Migrator> migrators,
             boolean forceOverride,
             int version) {
         mSchemas = Preconditions.checkNotNull(schemas);
         mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
         mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
+        mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);
         mMigrators = Preconditions.checkNotNull(migrators);
         mForceOverride = forceOverride;
         mVersion = version;
@@ -125,13 +223,47 @@
     @NonNull
     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
         Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
-        for (String key : mSchemasVisibleToPackages.keySet()) {
-            copy.put(key, new ArraySet<>(mSchemasVisibleToPackages.get(key)));
+        for (Map.Entry<String, Set<PackageIdentifier>> entry :
+                mSchemasVisibleToPackages.entrySet()) {
+            copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
         }
         return copy;
     }
 
     /**
+     * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
+     * combinations that querier must hold to access that schema type.
+     *
+     * <p> The querier could read the {@link GenericDocument} objects under the {@code schemaType}
+     * if they holds ALL required permissions of ANY of the individual value sets.
+     *
+     * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
+     * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
+     * <ul>
+     *     <li>A querier holds both PermissionA and PermissionB has access.</li>
+     *     <li>A querier holds both PermissionC and PermissionD has access.</li>
+     *     <li>A querier holds only PermissionE has access.</li>
+     *     <li>A querier holds both PermissionA and PermissionE has access.</li>
+     *     <li>A querier holds only PermissionA doesn't have access.</li>
+     *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
+     * </ul>
+     *
+     * <p>It’s inefficient to call this method repeatedly.
+     *
+     * @return The map contains schema type and all combinations of required permission for querier
+     *         to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS},
+     *         {@link SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS},
+     *         {@link SetSchemaRequest#READ_EXTERNAL_STORAGE},
+     *         {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
+     *         {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
+     */
+    @SetSchemaRequest.AppSearchSupportedPermission
+    @NonNull
+    public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
+        return deepCopy(mSchemasVisibleToPermissions);
+    }
+
+    /**
      * Returns the map of {@link Migrator}, the key will be the schema type of the
      * {@link Migrator} associated with.
      */
@@ -174,6 +306,7 @@
         private ArraySet<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
         private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
                 new ArrayMap<>();
+        private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
         private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
         private boolean mForceOverride = false;
         private int mVersion = DEFAULT_VERSION;
@@ -255,7 +388,7 @@
          * and visible on any system UI surface.
          *
          * <p>This setting applies to the provided {@code schemaType} only, and does not persist
-         * across {@link AppSearchSession#setSchema} calls.
+         * across {@link AppSearchSession#setSchemaAsync} calls.
          *
          * <p>The default behavior, if this method is not called, is to allow types to be
          * displayed on system UI surfaces.
@@ -280,6 +413,72 @@
         }
 
         /**
+         * Adds a set of required Android {@link android.Manifest.permission} combination to the
+         * given schema type.
+         *
+         * <p> If the querier holds ALL of the required permissions in this combination, they will
+         * have access to read {@link GenericDocument} objects of the given schema type.
+         *
+         * <p> You can call this method to add multiple permission combinations, and the querier
+         * will have access if they holds ANY of the combinations.
+         *
+         * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
+         * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
+         * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
+         *
+         * @see android.Manifest.permission#READ_SMS
+         * @see android.Manifest.permission#READ_CALENDAR
+         * @see android.Manifest.permission#READ_CONTACTS
+         * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+         * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+         * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+         * @param schemaType       The schema type to set visibility on.
+         * @param permissions      A set of required Android permissions the caller need to hold
+         *                         to access {@link GenericDocument} objects that under the given
+         *                         schema.
+         * @throws IllegalArgumentException – if input unsupported permission.
+         */
+        // Merged list available from getRequiredPermissionsForSchemaTypeVisibility
+        @SuppressLint("MissingGetterMatchingBuilder")
+        // @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+        // @exportToFramework:endStrip()
+        @NonNull
+        public Builder addRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType,
+                @AppSearchSupportedPermission @NonNull Set<Integer> permissions) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(permissions);
+            for (int permission : permissions) {
+                Preconditions.checkArgumentInRange(permission, READ_SMS,
+                        READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+            }
+            resetIfBuilt();
+            Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
+            if (visibleToPermissions == null) {
+                visibleToPermissions = new ArraySet<>();
+                mSchemasVisibleToPermissions.put(schemaType, visibleToPermissions);
+            }
+            visibleToPermissions.add(permissions);
+            return this;
+        }
+
+        /**  Clears all required permissions combinations for the given schema type.  */
+        // @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+        // @exportToFramework:endStrip()
+        @NonNull
+        public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            resetIfBuilt();
+            mSchemasVisibleToPermissions.remove(schemaType);
+            return this;
+        }
+
+        /**
          * Sets whether or not documents from the provided {@code schemaType} can be read by the
          * specified package.
          *
@@ -353,7 +552,7 @@
          *
          * @see SetSchemaRequest.Builder#setVersion
          * @see SetSchemaRequest.Builder#addSchemas
-         * @see AppSearchSession#setSchema
+         * @see AppSearchSession#setSchemaAsync
          */
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")        // Getter return plural objects.
@@ -388,7 +587,7 @@
          *
          * @see SetSchemaRequest.Builder#setVersion
          * @see SetSchemaRequest.Builder#addSchemas
-         * @see AppSearchSession#setSchema
+         * @see AppSearchSession#setSchemaAsync
          */
         @NonNull
         public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
@@ -406,7 +605,7 @@
          * 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}
+         * annotated class only, and does not persist across {@link AppSearchSession#setSchemaAsync}
          * calls.
          *
          * <p>The default behavior, if this method is not called, is to allow types to be
@@ -469,6 +668,66 @@
             return setSchemaTypeVisibilityForPackage(factory.getSchemaName(), visible,
                     packageIdentifier);
         }
+
+        /**
+         * Adds a set of required Android {@link android.Manifest.permission} combination to the
+         * given schema type.
+         *
+         * <p> If the querier holds ALL of the required permissions in this combination, they will
+         * have access to read {@link GenericDocument} objects of the given schema type.
+         *
+         * <p> You can call this method to add multiple permission combinations, and the querier
+         * will have access if they holds ANY of the combinations.
+         *
+         * <p>The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR},
+         * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
+         * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
+         *
+         * @see android.Manifest.permission#READ_SMS
+         * @see android.Manifest.permission#READ_CALENDAR
+         * @see android.Manifest.permission#READ_CONTACTS
+         * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
+         * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
+         * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
+         * @param documentClass    The {@link androidx.appsearch.annotation.Document} class to set
+         *                         visibility on.
+         * @param permissions      A set of required Android permissions the caller need to hold
+         *                         to access {@link GenericDocument} objects that under the given
+         *                         schema.
+         * @throws IllegalArgumentException – if input unsupported permission.
+         */
+        // Merged map available from getRequiredPermissionsForSchemaTypeVisibility
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+        @NonNull
+        public Builder addRequiredPermissionsForDocumentClassVisibility(
+                @NonNull Class<?> documentClass,
+                @AppSearchSupportedPermission @NonNull Set<Integer> permissions)
+                throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return addRequiredPermissionsForSchemaTypeVisibility(
+                    factory.getSchemaName(), permissions);
+        }
+
+        /**  Clears all required permissions combinations for the given schema type.  */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+        @NonNull
+        public Builder clearRequiredPermissionsForDocumentClassVisibility(
+                @NonNull Class<?> documentClass)
+                throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName());
+        }
 // @exportToFramework:endStrip()
 
         /**
@@ -497,7 +756,7 @@
          *
          * <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
+         * {@link AppSearchSession#setSchemaAsync} 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.
          *
@@ -513,7 +772,7 @@
          *
          * @throws IllegalArgumentException if the version is negative.
          *
-         * @see AppSearchSession#setSchema
+         * @see AppSearchSession#setSchemaAsync
          * @see Migrator
          * @see SetSchemaRequest.Builder#setMigrator
          */
@@ -539,6 +798,7 @@
             // Create a copy because we're going to remove from the set for verification purposes.
             Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
+            referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
 
             for (AppSearchSchema schema : mSchemas) {
                 referencedSchemas.remove(schema.getSchemaType());
@@ -558,6 +818,7 @@
                     mSchemas,
                     mSchemasNotDisplayedBySystem,
                     mSchemasVisibleToPackages,
+                    mSchemasVisibleToPermissions,
                     mMigrators,
                     mForceOverride,
                     mVersion);
@@ -573,6 +834,8 @@
                 }
                 mSchemasVisibleToPackages = schemasVisibleToPackages;
 
+                mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
+
                 mSchemas = new ArraySet<>(mSchemas);
                 mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
                 mMigrators = new ArrayMap<>(mMigrators);
@@ -580,4 +843,17 @@
             }
         }
     }
+
+    static ArrayMap<String, Set<Set<Integer>>> deepCopy(@NonNull Map<String,
+            Set<Set<Integer>>> original) {
+        ArrayMap<String, Set<Set<Integer>>> copy = new ArrayMap<>(original.size());
+        for (Map.Entry<String, Set<Set<Integer>>> entry : original.entrySet()) {
+            Set<Set<Integer>> valueCopy = new ArraySet<>();
+            for (Set<Integer> innerValue : entry.getValue()) {
+                valueCopy.add(new ArraySet<>(innerValue));
+            }
+            copy.put(entry.getKey(), valueCopy);
+        }
+        return copy;
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
index 88e6645..3f8ce5e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -30,7 +30,7 @@
 import java.util.List;
 import java.util.Set;
 
-/** The response class of {@link AppSearchSession#setSchema} */
+/** The response class of {@link AppSearchSession#setSchemaAsync} */
 public class SetSchemaResponse {
 
     private static final String DELETED_TYPES_FIELD = "deletedTypes";
@@ -102,7 +102,7 @@
      *
      * <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.
+     * {@link AppSearchSession#setSchemaAsync} call.
      *
      * <p>Documents for a deleted type are removed from the database.
      */
@@ -117,7 +117,7 @@
 
     /**
      * Returns a {@link Set} of schema type that were migrated by the
-     * {@link AppSearchSession#setSchema} call.
+     * {@link AppSearchSession#setSchemaAsync} 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
@@ -139,13 +139,13 @@
 
     /**
      * Returns a {@link Set} of schema type whose new definitions set in the
-     * {@link AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
+     * {@link AppSearchSession#setSchemaAsync} 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 AppSearchSession#setSchemaAsync
      * @see SetSchemaRequest.Builder#setForceOverride
      */
     @NonNull
@@ -281,7 +281,7 @@
 
     /**
      * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
-     * {@link AppSearchSession#setSchema}.
+     * {@link AppSearchSession#setSchemaAsync}.
      */
     public static class MigrationFailure {
         private static final String SCHEMA_TYPE_FIELD = "schemaType";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
new file mode 100644
index 0000000..6f04dc5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
@@ -0,0 +1,259 @@
+/*
+ * 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.
+ */
+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.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Holds the visibility settings that apply to a schema type.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityDocument extends GenericDocument {
+    /**
+     * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+     */
+    public static final String SCHEMA_TYPE = "VisibilityType";
+    /** Namespace of documents that contain visibility settings */
+    public static final String NAMESPACE = "";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+    /** Property that holds the package name that can access a schema. */
+    private static final String PACKAGE_NAME_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
+
+    /** Property that holds the required permissions to access the schema. */
+    private static final String PERMISSION_PROPERTY = "permission";
+
+    // The initial schema version, one VisibilityDocument contains all visibility information for
+    // whole package.
+    public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+    // One VisibilityDocument contains visibility information for a single schema.
+    public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+    // One VisibilityDocument contains visibility information for a single schema.
+    public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+    public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * <p>NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+     */
+    public static final AppSearchSchema
+            SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                    NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                    .build())
+            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(PERMISSION_PROPERTY,
+                    VisibilityPermissionDocument.SCHEMA_TYPE)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .build();
+
+    public VisibilityDocument(@NonNull GenericDocument genericDocument) {
+        super(genericDocument);
+    }
+
+    public VisibilityDocument(@NonNull Bundle bundle) {
+        super(bundle);
+    }
+
+    /** Returns whether this schema is visible to the system. */
+    public boolean isNotDisplayedBySystem() {
+        return getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
+    }
+
+    /**
+     * Returns a package name array which could access this schema. Use {@link #getSha256Certs()}
+     * to get package's sha 256 certs. The same index of package names array and sha256Certs array
+     * represents same package.
+     */
+    @NonNull
+    public String[] getPackageNames() {
+        return getPropertyStringArray(PACKAGE_NAME_PROPERTY);
+    }
+
+    /**
+     * Returns a package sha256Certs array which could access this schema. Use
+     * {@link #getPackageNames()} to get package's name. The same index of package names array
+     * and sha256Certs array represents same package.
+     */
+    @NonNull
+    public byte[][] getSha256Certs() {
+        return getPropertyBytesArray(SHA_256_CERT_PROPERTY);
+    }
+
+    /**
+     * Returns an array of Android Permissions that caller mush hold to access the schema
+     * this {@link VisibilityDocument} represents.
+     */
+    @Nullable
+    public Set<Set<Integer>> getVisibleToPermissions() {
+        GenericDocument[] permissionDocuments = getPropertyDocumentArray(PERMISSION_PROPERTY);
+        if (permissionDocuments == null) {
+            return Collections.emptySet();
+        }
+        Set<Set<Integer>> visibleToPermissions = new ArraySet<>(permissionDocuments.length);
+        for (GenericDocument permissionDocument : permissionDocuments) {
+            visibleToPermissions.add(
+                    new VisibilityPermissionDocument(permissionDocument)
+                            .getAllRequiredPermissions());
+        }
+        return visibleToPermissions;
+    }
+
+    /** Builder for {@link VisibilityDocument}. */
+    public static class Builder extends GenericDocument.Builder<Builder> {
+        private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
+
+        /**
+         * Creates a {@link Builder} for a {@link VisibilityDocument}.
+         *
+         * @param id The SchemaType of the {@link AppSearchSchema} that this
+         *           {@link VisibilityDocument} represents. The package and database prefix will be
+         *           added in server side. We are using prefixed schema type to be the final id of
+         *           this {@link VisibilityDocument}.
+         */
+        public Builder(@NonNull String id) {
+            super(NAMESPACE, id, SCHEMA_TYPE);
+        }
+
+        /** Sets whether this schema has opted out of platform surfacing. */
+        @NonNull
+        public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+            return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+                    notDisplayedBySystem);
+        }
+
+        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @NonNull
+        public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
+            Preconditions.checkNotNull(packageIdentifiers);
+            mPackageIdentifiers.addAll(packageIdentifiers);
+            return this;
+        }
+
+        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @NonNull
+        public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+            Preconditions.checkNotNull(packageIdentifier);
+            mPackageIdentifiers.add(packageIdentifier);
+            return this;
+        }
+
+        /**
+         * Set required permission sets for a package needs to hold to the schema this
+         * {@link VisibilityDocument} represents.
+         *
+         * <p> The querier could have access if they holds ALL required permissions of ANY of the
+         * individual value sets.
+         */
+        @NonNull
+        public Builder setVisibleToPermissions(@NonNull Set<Set<Integer>> visibleToPermissions) {
+            Preconditions.checkNotNull(visibleToPermissions);
+            VisibilityPermissionDocument[] permissionDocuments =
+                    new VisibilityPermissionDocument[visibleToPermissions.size()];
+            int i = 0;
+            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+                permissionDocuments[i++] = new VisibilityPermissionDocument
+                        .Builder(NAMESPACE, /*id=*/String.valueOf(i))
+                        .setVisibleToAllRequiredPermissions(allRequiredPermissions)
+                        .build();
+            }
+            setPropertyDocument(PERMISSION_PROPERTY, permissionDocuments);
+            return this;
+        }
+
+        /** Build a {@link VisibilityDocument} */
+        @Override
+        @NonNull
+        public VisibilityDocument build() {
+            String[] packageNames = new String[mPackageIdentifiers.size()];
+            byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32];
+            int i = 0;
+            for (PackageIdentifier packageIdentifier : mPackageIdentifiers) {
+                packageNames[i] = packageIdentifier.getPackageName();
+                sha256Certs[i] = packageIdentifier.getSha256Certificate();
+                ++i;
+            }
+            setPropertyString(PACKAGE_NAME_PROPERTY, packageNames);
+            setPropertyBytes(SHA_256_CERT_PROPERTY, sha256Certs);
+            return new VisibilityDocument(super.build());
+        }
+    }
+
+
+    /**  Build the List of {@link VisibilityDocument} from visibility settings. */
+    @NonNull
+    public static List<VisibilityDocument> toVisibilityDocuments(
+            @NonNull SetSchemaRequest setSchemaRequest) {
+        Set<AppSearchSchema> searchSchemas = setSchemaRequest.getSchemas();
+        Set<String> schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();
+        Map<String, Set<PackageIdentifier>> schemasVisibleToPackages =
+                setSchemaRequest.getSchemasVisibleToPackages();
+        Map<String, Set<Set<Integer>>> schemasVisibleToPermissions =
+                setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();
+
+        List<VisibilityDocument> visibilityDocuments = new ArrayList<>(searchSchemas.size());
+
+        for (AppSearchSchema searchSchema : searchSchemas) {
+            String schemaType = searchSchema.getSchemaType();
+            VisibilityDocument.Builder documentBuilder =
+                    new VisibilityDocument.Builder(/*id=*/searchSchema.getSchemaType());
+            documentBuilder.setNotDisplayedBySystem(
+                    schemasNotDisplayedBySystem.contains(schemaType));
+
+            if (schemasVisibleToPackages.containsKey(schemaType)) {
+                documentBuilder.addVisibleToPackages(schemasVisibleToPackages.get(schemaType));
+            }
+
+            if (schemasVisibleToPermissions.containsKey(schemaType)) {
+                documentBuilder.setVisibleToPermissions(
+                        schemasVisibleToPermissions.get(schemaType));
+            }
+            visibilityDocuments.add(documentBuilder.build());
+        }
+        return visibilityDocuments;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
new file mode 100644
index 0000000..e859cb9
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+
+import java.util.Set;
+
+/**
+ * The nested document that holds all required permissions for a caller need to hold to access the
+ * schema which the outer {@link VisibilityDocument} represents.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityPermissionDocument extends GenericDocument {
+
+    /**
+     * The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings.
+     */
+    public static final String SCHEMA_TYPE = "VisibilityPermissionType";
+
+    /** Property that holds the required permissions to access the schema. */
+    private static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * <p>NOTE: If you update this, also update {@link VisibilityDocument#SCHEMA_VERSION_LATEST}.
+     */
+    public static final AppSearchSchema
+            SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
+            .addProperty(new AppSearchSchema.LongPropertyConfig
+                    .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY)
+                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                    .build())
+            .build();
+
+    VisibilityPermissionDocument(@NonNull GenericDocument genericDocument) {
+        super(genericDocument);
+    }
+
+    /**
+     * Returns an array of Android Permissions that caller mush hold to access the schema
+     * that the outer {@link VisibilityDocument} represents.
+     */
+    @Nullable
+    public Set<Integer> getAllRequiredPermissions() {
+        return toInts(getPropertyLongArray(ALL_REQUIRED_PERMISSIONS_PROPERTY));
+    }
+
+    /** Builder for {@link VisibilityPermissionDocument}. */
+    public static class Builder extends GenericDocument.Builder<Builder> {
+
+        /**
+         * Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            super(namespace, id, SCHEMA_TYPE);
+        }
+
+        /** Sets whether this schema has opted out of platform surfacing. */
+        @NonNull
+        public Builder setVisibleToAllRequiredPermissions(
+                @NonNull Set<Integer> allRequiredPermissions) {
+            setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, toLongs(allRequiredPermissions));
+            return this;
+        }
+
+        /** Build a {@link VisibilityPermissionDocument} */
+        @Override
+        @NonNull
+        public VisibilityPermissionDocument build() {
+            return new VisibilityPermissionDocument(super.build());
+        }
+    }
+
+    @NonNull
+    static long[] toLongs(@NonNull Set<Integer> properties) {
+        long[] outputs = new long[properties.size()];
+        int i = 0;
+        for (int property : properties) {
+            outputs[i++] = property;
+        }
+        return outputs;
+    }
+
+    @Nullable
+    private static Set<Integer> toInts(@Nullable long[] properties) {
+        if (properties == null) {
+            return null;
+        }
+        Set<Integer> outputs = new ArraySet<>(properties.length);
+        for (long property : properties) {
+            outputs.add((int) property);
+        }
+        return outputs;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
index c040aa2..c01917e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/AppSearchObserverCallback.java
@@ -16,23 +16,13 @@
 
 package androidx.appsearch.observer;
 
-import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 /**
- * An interface which apps can implement to subscribe to notifications of changes to AppSearch data.
+ * @deprecated use {@link ObserverCallback} instead.
+ * @hide
  */
-public interface AppSearchObserverCallback {
-    /**
-     * Callback to trigger after schema changes (schema type added, updated or removed).
-     *
-     * @param changeInfo Information about the nature of the change.
-     */
-    void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo);
-
-    /**
-     * Callback to trigger after document changes (documents added, updated or removed).
-     *
-     * @param changeInfo Information about the nature of the change.
-     */
-    void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo);
-}
+// TODO(b/209734214): Remove this after dogfooders and devices have migrated away from this class.
+@Deprecated
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchObserverCallback extends ObserverCallback {}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
index 0fd9ae2..32d19ad 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/DocumentChangeInfo.java
@@ -21,8 +21,11 @@
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
+import java.util.Set;
+
 /**
- * Contains information about an individual change detected by an {@link AppSearchObserverCallback}.
+ * Contains information about an individual change detected by an {@link ObserverCallback}.
  *
  * <p>This class reports information about document changes, i.e. when documents were added, updated
  * or removed.
@@ -30,8 +33,11 @@
  * <p>Changes are grouped by package, database, schema type and namespace. Each unique
  * combination of these items will generate a unique {@link DocumentChangeInfo}.
  *
+ * <p>Notifications are only sent for documents whose schema type matches an observer's schema
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}).
+ *
  * <p>Note that document changes that happen during schema migration from calling
- * {@link androidx.appsearch.app.AppSearchSession#setSchema} are not reported via this class.
+ * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync} are not reported via this class.
  * Such changes are reported through {@link SchemaChangeInfo}.
  */
 public final class DocumentChangeInfo {
@@ -39,8 +45,7 @@
     private final String mDatabase;
     private final String mNamespace;
     private final String mSchemaName;
-
-    // TODO(b/193494000): Add the set of changed document IDs to this class
+    private final Set<String> mChangedDocumentIds;
 
     /**
      * Constructs a new {@link DocumentChangeInfo}.
@@ -49,16 +54,21 @@
      * @param database The database in which the documents that changed reside.
      * @param namespace    The namespace in which the documents that changed reside.
      * @param schemaName   The name of the schema type that contains the changed documents.
+     * @param changedDocumentIds The set of document IDs that have been changed as part of this
+     *                   notification.
      */
     public DocumentChangeInfo(
             @NonNull String packageName,
             @NonNull String database,
             @NonNull String namespace,
-            @NonNull String schemaName) {
+            @NonNull String schemaName,
+            @NonNull Set<String> changedDocumentIds) {
         mPackageName = Preconditions.checkNotNull(packageName);
         mDatabase = Preconditions.checkNotNull(database);
         mNamespace = Preconditions.checkNotNull(namespace);
         mSchemaName = Preconditions.checkNotNull(schemaName);
+        mChangedDocumentIds = Collections.unmodifiableSet(
+                Preconditions.checkNotNull(changedDocumentIds));
     }
 
     /** Returns the package name of the app which owns the documents that changed. */
@@ -85,6 +95,16 @@
         return mSchemaName;
     }
 
+    /**
+     * Returns the set of document IDs that have been changed as part of this notification.
+     *
+     * <p>This will never be empty.
+     */
+    @NonNull
+    public Set<String> getChangedDocumentIds() {
+        return mChangedDocumentIds;
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
@@ -93,12 +113,14 @@
         return mPackageName.equals(that.mPackageName)
                 && mDatabase.equals(that.mDatabase)
                 && mNamespace.equals(that.mNamespace)
-                && mSchemaName.equals(that.mSchemaName);
+                && mSchemaName.equals(that.mSchemaName)
+                && mChangedDocumentIds.equals(that.mChangedDocumentIds);
     }
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(mPackageName, mDatabase, mNamespace, mSchemaName);
+        return ObjectsCompat.hash(
+                mPackageName, mDatabase, mNamespace, mSchemaName, mChangedDocumentIds);
     }
 
     @NonNull
@@ -109,6 +131,7 @@
                 + ", database='" + mDatabase + '\''
                 + ", namespace='" + mNamespace + '\''
                 + ", schemaName='" + mSchemaName + '\''
+                + ", changedDocumentIds='" + mChangedDocumentIds + '\''
                 + '}';
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.java
new file mode 100644
index 0000000..cae945a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverCallback.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.observer;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An interface which apps can implement to subscribe to notifications of changes to AppSearch data.
+ */
+public interface ObserverCallback {
+    /**
+     * Callback to trigger after schema changes (schema type added, updated or removed).
+     *
+     * @param changeInfo Information about the nature of the change.
+     */
+    void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo);
+
+    /**
+     * Callback to trigger after document changes (documents added, updated or removed).
+     *
+     * @param changeInfo Information about the nature of the change.
+     */
+    void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo);
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
index e8979e5..6e3705e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
@@ -37,8 +37,8 @@
 import java.util.Set;
 
 /**
- * Configures the types, namespaces and other properties that {@link AppSearchObserverCallback}
- * instances match against.
+ * Configures the types, namespaces and other properties that {@link ObserverCallback} instances
+ * match against.
  */
 public final class ObserverSpec {
     private static final String FILTER_SCHEMA_FIELD = "filterSchema";
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
index 6eff8c4..e912cf8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/SchemaChangeInfo.java
@@ -21,11 +21,15 @@
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
+import java.util.Set;
+
 /**
- * Contains information about a schema change detected by an {@link AppSearchObserverCallback}.
+ * Contains information about a schema change detected by an {@link ObserverCallback}.
  *
  * <p>This object will be sent when a schema type having a name matching an observer's schema
- * type filters has been added, updated, or removed.
+ * filters (as determined by {@link ObserverSpec#getFilterSchemas}) has been added, updated, or
+ * removed.
  *
  * <p>Note that schema changes may cause documents to be migrated or removed. When this happens,
  * individual document updates will NOT be dispatched via {@link DocumentChangeInfo}. The only
@@ -35,18 +39,23 @@
 public final class SchemaChangeInfo {
     private final String mPackageName;
     private final String mDatabaseName;
-
-    // TODO(b/193494000): Add the set of changed schema names to this class
+    private final Set<String> mChangedSchemaNames;
 
     /**
      * Constructs a new {@link SchemaChangeInfo}.
      *
      * @param packageName     The package name of the app which owns the schema that changed.
      * @param databaseName    The database in which the schema that changed resides.
+     * @param changedSchemaNames Names of schemas that have changed as part of this notification.
      */
-    public SchemaChangeInfo(@NonNull String packageName, @NonNull String databaseName) {
+    public SchemaChangeInfo(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Set<String> changedSchemaNames) {
         mPackageName = Preconditions.checkNotNull(packageName);
         mDatabaseName = Preconditions.checkNotNull(databaseName);
+        mChangedSchemaNames = Collections.unmodifiableSet(
+                Preconditions.checkNotNull(changedSchemaNames));
     }
 
     /** Returns the package name of the app which owns the schema that changed. */
@@ -61,17 +70,29 @@
         return mDatabaseName;
     }
 
+    /**
+     * Returns the names of schema types affected by this change notification.
+     *
+     * <p>This will never be empty.
+     */
+    @NonNull
+    public Set<String> getChangedSchemaNames() {
+        return mChangedSchemaNames;
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
         if (!(o instanceof SchemaChangeInfo)) return false;
         SchemaChangeInfo that = (SchemaChangeInfo) o;
-        return mPackageName.equals(that.mPackageName) && mDatabaseName.equals(that.mDatabaseName);
+        return mPackageName.equals(that.mPackageName)
+                && mDatabaseName.equals(that.mDatabaseName)
+                && mChangedSchemaNames.equals(that.mChangedSchemaNames);
     }
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(mPackageName, mDatabaseName);
+        return ObjectsCompat.hash(mPackageName, mDatabaseName, mChangedSchemaNames);
     }
 
     @NonNull
@@ -80,6 +101,7 @@
         return "SchemaChangeInfo{"
                 + "packageName='" + mPackageName + '\''
                 + ", databaseName='" + mDatabaseName + '\''
+                + ", changedSchemaNames='" + mChangedSchemaNames + '\''
                 + '}';
     }
 }
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index e3d4c59..48ec64b 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -71,7 +71,7 @@
         '../../../prebuilts/tools/common/google-java-format/google-java-format')
 
 # Miscellaneous constants
-CHANGEID_FILE_NAME = 'synced_jetpack_changeid.txt'
+SHA_FILE_NAME = 'synced_jetpack_sha.txt'
 
 
 class ExportToFramework:
@@ -96,6 +96,10 @@
             print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
             return
 
+        copyToPath = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
+        if copyToPath:
+          dest_path = os.path.join(self._framework_appsearch_root, copyToPath.group(1))
+
         print('Copy: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
         if transform_func:
             contents = transform_func(contents)
@@ -133,7 +137,7 @@
                     flags=re.MULTILINE)
 
         # Apply in-place replacements
-        return (contents
+        contents = (contents
             .replace('androidx.appsearch.app', 'android.app.appsearch')
             .replace(
                     'androidx.appsearch.localstorage.',
@@ -161,11 +165,20 @@
             .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()', '')
         )
+        contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
+
+        # Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
+        # to allow the same documentation to compile for both.
+        contents = re.sub(r'(#[a-zA-Z0-9_]+)Async}', r'\1}', contents)
+        contents = re.sub(
+                r'(\@see [^#]+#[a-zA-Z0-9_]+)Async$', r'\1', contents, flags=re.MULTILINE)
+        return contents
 
     def _TransformTestCode(self, contents):
         contents = (contents
@@ -318,19 +331,26 @@
         self._ExportImplCode()
         self._FormatWrittenFiles()
 
-    def WriteChangeIdFile(self, changeid):
-        """Copies the changeid of the most recent public CL into a file on the framework side.
+    def WriteShaFile(self, sha):
+        """Copies the git sha of the most recent public CL into a file on the framework side.
 
         This file is used for tracking, to determine what framework is synced to.
 
-        You must always provide a changeid of an exported, preferably even submitted CL. If you
-        abandon the CL pointed to by this changeid, the next person syncing framework will be unable
-        to find what CL it is synced to.
+        You must always provide a sha of a submitted submitted git commit. If you abandon the CL
+        pointed to by this sha, the next person syncing framework will be unable to find what CL it
+        is synced to.
+
+        The previous content of the sha file, if any, is returned.
         """
-        file_path = os.path.join(self._framework_appsearch_root, CHANGEID_FILE_NAME)
+        file_path = os.path.join(self._framework_appsearch_root, SHA_FILE_NAME)
+        old_sha = None
+        if os.path.isfile(file_path):
+          with open(file_path, 'r') as fh:
+              old_sha = fh.read().rstrip()
         with open(file_path, 'w') as fh:
-            print(changeid, file=fh)
+            print(sha, file=fh)
         print('Wrote "%s"' % file_path)
+        return old_sha
 
 
 if __name__ == '__main__':
@@ -339,6 +359,12 @@
                   sys.argv[0]),
               file=sys.stderr)
         sys.exit(1)
+    if sys.argv[2].startswith('I'):
+        print('Error: Git sha "%s" looks like a changeid. Please provide a git sha instead.' % (
+                  sys.argv[2]),
+              file=sys.stderr)
+        sys.exit(1)
+
     source_dir = os.path.normpath(os.path.dirname(sys.argv[0]))
     dest_dir = os.path.normpath(sys.argv[1])
     dest_dir = os.path.join(dest_dir, 'packages/modules/AppSearch')
@@ -349,4 +375,10 @@
         sys.exit(1)
     exporter = ExportToFramework(source_dir, dest_dir)
     exporter.ExportCode()
-    exporter.WriteChangeIdFile(sys.argv[2])
+
+    # Update the sha file
+    new_sha = sys.argv[2]
+    old_sha = exporter.WriteShaFile(new_sha)
+    if old_sha and old_sha != new_sha:
+      print('Command to diff old version to new version:')
+      print('  git log %s..%s -- appsearch/' % (old_sha, new_sha))
diff --git a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
index 862a9ff..88036ff 100644
--- a/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/browseractions/BrowserActionsIntent.java
@@ -381,6 +381,7 @@
      * @hide
      */
     @RestrictTo(LIBRARY)
+    @SuppressWarnings("deprecation")
     @NonNull
     public static List<ResolveInfo> getBrowserActionsIntentHandlers(@NonNull Context context) {
         Intent intent =
@@ -389,7 +390,7 @@
         return pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
     }
 
-    @SuppressWarnings("NullAway") // TODO: b/141869398
+    @SuppressWarnings({"NullAway", "deprecation"}) // TODO: b/141869398
     private static void openFallbackBrowserActionsMenu(Context context, Intent intent) {
         Uri uri = intent.getData();
         ArrayList<Bundle> bundles = intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
index 40386b6..f1675f2 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
@@ -86,6 +86,7 @@
      *               {@link CustomTabsIntent#EXTRA_SESSION}.
      * @return The token that was generated.
      */
+    @SuppressWarnings("deprecation")
     public static @Nullable CustomTabsSessionToken getSessionTokenFromIntent(
             @NonNull Intent intent) {
         Bundle b = intent.getExtras();
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java b/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
index b95bd78..37d604e 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/TrustedWebUtils.java
@@ -119,6 +119,7 @@
      * @return Whether the specified Custom Tabs provider supports the specified splash screen
      *         feature/version.
      */
+    @SuppressWarnings("deprecation")
     public static boolean areSplashScreensSupported(@NonNull Context context,
             @NonNull String packageName, @NonNull String version) {
         Intent serviceIntent = new Intent()
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index f3fd01e..4a5ac59 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -274,6 +274,7 @@
         assertNullSessionInExtras(intent);
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void putsSessionBinderAndId_IfSuppliedInConstructor() {
         CustomTabsSession session = TestUtil.makeMockSession();
@@ -283,6 +284,7 @@
         assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void putsSessionBinderAndId_IfSuppliedInSetter() {
         CustomTabsSession session = TestUtil.makeMockSession();
@@ -292,6 +294,7 @@
         assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void putsPendingSessionId() {
         CustomTabsSession.PendingSession pendingSession = TestUtil.makeMockPendingSession();
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
index 318a99a..fa0c171 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
@@ -55,6 +55,7 @@
         return PendingIntent.getBroadcast(mock(Context.class), 0, new Intent(), 0);
     }
 
+    @SuppressWarnings("deprecation")
     public static void assertIntentHasSession(@NonNull Intent intent,
             @NonNull CustomTabsSession session) {
         assertEquals(session.getBinder(), intent.getExtras().getBinder(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index f58237b..0940177 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -269,6 +269,10 @@
         // Reenable after upgradingto 7.1.0-beta01
         disable.add("SupportAnnotationUsage")
 
+        // Broken when building with compileSdk 31, b/208451611
+        disable.add("VectorPath")
+        disable.add("InvalidVectorPath")
+
         // Provide stricter enforcement for project types intended to run on a device.
         if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
             fatal.add("Assert")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index 48197b5..22e79ea 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -35,7 +35,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-31"
+    const val COMPILE_SDK_VERSION = "android-Tiramisu"
 
     /**
      * The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
index c3a2051..89dbe9f 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
@@ -48,7 +48,7 @@
     private val imageValidationActivityRequestCode =
         ImageValidationActivity::class.java.hashCode() % 1000
 
-    @Suppress("UNCHECKED_CAST")
+    @Suppress("UNCHECKED_CAST", "DEPRECATION")
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.full_listview)
diff --git a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
index 04e9656..0b75733 100644
--- a/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
+++ b/car/app/app-projected/src/test/java/androidx/car/app/hardware/common/CarResultStubMapTest.java
@@ -121,7 +121,7 @@
         Integer desiredResult = 5;
         Bundleable desiredBundleable = Bundleable.create(desiredResult);
         int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
-        Integer unsupportedResult = new Integer(-1);
+        Integer unsupportedResult = -1;
         String param = "param";
         Bundleable paramBundle = Bundleable.create(param);
 
@@ -193,7 +193,7 @@
     public void addListener_multiple_listener_multiple_param() throws BundlerException,
             RemoteException {
         int desiredResultType = ICarHardwareResultTypes.TYPE_SENSOR_ACCELEROMETER;
-        Integer unsupportedResult = new Integer(-1);
+        Integer unsupportedResult = -1;
 
         CarResultStubMap<Integer, Integer> carResultStubMap = new CarResultStubMap<>(
                 desiredResultType,
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
index 2036e2b..d502dd9 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
@@ -30,6 +30,7 @@
 import androidx.car.app.constraints.ConstraintManager;
 import androidx.car.app.model.CarColor;
 import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.CarIconSpan;
 import androidx.car.app.model.CarLocation;
 import androidx.car.app.model.Distance;
 import androidx.car.app.model.DistanceSpan;
@@ -93,6 +94,17 @@
                     0,
                     1,
                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            if (index == 4) {
+                description.setSpan(CarIconSpan.create(new CarIcon.Builder(
+                                IconCompat.createWithBitmap(
+                                        BitmapFactory.decodeResource(
+                                                carContext.getResources(),
+                                                R.drawable.ic_hi)))
+                                .build(), CarIconSpan.ALIGN_BOTTOM),
+                        5,
+                        6,
+                        Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            }
 
             boolean isBrowsable = index > mPlaces.size() / 2;
 
@@ -225,52 +237,84 @@
                                 .build()));
 
         Location location5 = new Location(SamplePlaces.class.getSimpleName());
-        location5.setLatitude(47.6490374);
-        location5.setLongitude(-122.3527127);
+        location5.setLatitude(37.422014);
+        location5.setLongitude(-122.084776);
+        SpannableString title5 = new SpannableString(" ");
+        title5.setSpan(CarIconSpan.create(new CarIcon.Builder(
+                IconCompat.createWithBitmap(
+                        BitmapFactory.decodeResource(
+                                carContext.getResources(),
+                                R.drawable.ic_hi)))
+                .build(), CarIconSpan.ALIGN_BOTTOM),
+                0,
+                1,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        SpannableString description5 = new SpannableString(" ");
+        places.add(
+                new PlaceInfo(
+                        title5,
+                        "1600 Amphitheatre Pkwy, Mountain View, CA 94043",
+                        description5,
+                        "+16502530000",
+                        location5,
+                        new PlaceMarker.Builder()
+                                .setIcon(
+                                        new CarIcon.Builder(
+                                                IconCompat.createWithBitmap(
+                                                        BitmapFactory.decodeResource(
+                                                                carContext.getResources(),
+                                                                R.drawable.test_image_square)))
+                                                .build(),
+                                        PlaceMarker.TYPE_IMAGE)
+                                .build()));
+
+        Location location6 = new Location(SamplePlaces.class.getSimpleName());
+        location6.setLatitude(47.6490374);
+        location6.setLongitude(-122.3527127);
         places.add(
                 new PlaceInfo(
                         "Google Bothell",
                         "11831 North Creek Pkwy, Bothell, WA 98011",
                         "Text label",
                         "n/a",
-                        location5,
+                        location6,
                         new PlaceMarker.Builder().build()));
 
         // Some hosts may display more items in the list than others, so create 3 more items.
-        Location location6 = new Location(SamplePlaces.class.getSimpleName());
-        location6.setLatitude(47.5496056);
-        location6.setLongitude(-122.2571713);
+        Location location7 = new Location(SamplePlaces.class.getSimpleName());
+        location7.setLatitude(47.5496056);
+        location7.setLongitude(-122.2571713);
         places.add(
                 new PlaceInfo(
                         "Seward Park",
                         "5900 Lake Washington Blvd S, Seattle, WA 98118",
                         "Text label",
                         "n/a",
-                        location6,
+                        location7,
                         new PlaceMarker.Builder().build()));
 
-        Location location7 = new Location(SamplePlaces.class.getSimpleName());
-        location7.setLatitude(47.5911456);
-        location7.setLongitude(-122.2256602);
+        Location location8 = new Location(SamplePlaces.class.getSimpleName());
+        location8.setLatitude(47.5911456);
+        location8.setLongitude(-122.2256602);
         places.add(
                 new PlaceInfo(
                         "Luther Burbank Park",
                         "2040 84th Ave SE, Mercer Island, WA 98040",
                         "Text label",
                         "n/a",
-                        location7,
+                        location8,
                         new PlaceMarker.Builder().build()));
 
-        Location location8 = new Location(SamplePlaces.class.getSimpleName());
-        location8.setLatitude(47.6785932);
-        location8.setLongitude(-122.2113821);
+        Location location9 = new Location(SamplePlaces.class.getSimpleName());
+        location9.setLatitude(47.6785932);
+        location9.setLongitude(-122.2113821);
         places.add(
                 new PlaceInfo(
                         "Heritage Park",
                         "111 Waverly Way, Kirkland, WA 98033",
                         "Text label",
                         "n/a",
-                        location8,
+                        location9,
                         new PlaceMarker.Builder().build()));
 
         return places;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 101fe8c..14d0d83 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -314,7 +314,7 @@
             if (titleText.isEmpty()) {
                 throw new IllegalArgumentException("The title cannot be null or empty");
             }
-            CarTextConstraints.TEXT_ONLY.validateOrThrow(titleText);
+            CarTextConstraints.TEXT_AND_ICON.validateOrThrow(titleText);
             mTitle = titleText;
             return this;
         }
@@ -334,7 +334,7 @@
             if (requireNonNull(title).isEmpty()) {
                 throw new IllegalArgumentException("The title cannot be null or empty");
             }
-            CarTextConstraints.TEXT_ONLY.validateOrThrow(title);
+            CarTextConstraints.TEXT_AND_ICON.validateOrThrow(title);
             mTitle = title;
             return this;
         }
@@ -410,7 +410,7 @@
         @NonNull
         public Builder addText(@NonNull CharSequence text) {
             CarText carText = CarText.create(requireNonNull(text));
-            CarTextConstraints.TEXT_WITH_COLORS.validateOrThrow(carText);
+            CarTextConstraints.TEXT_WITH_COLORS_AND_ICON.validateOrThrow(carText);
             mTexts.add(CarText.create(requireNonNull(text)));
             return this;
         }
@@ -424,7 +424,7 @@
          */
         @NonNull
         public Builder addText(@NonNull CarText text) {
-            CarTextConstraints.TEXT_WITH_COLORS.validateOrThrow(requireNonNull(text));
+            CarTextConstraints.TEXT_WITH_COLORS_AND_ICON.validateOrThrow(requireNonNull(text));
             mTexts.add(text);
             return this;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
index bba4a06..2346dcb 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/CarTextConstraints.java
@@ -89,6 +89,15 @@
                     DurationSpan.class,
                     ForegroundCarColorSpan.class));
 
+    /** Allow text with color and icon {@link CarSpan}s. */
+    @NonNull
+    public static final CarTextConstraints TEXT_WITH_COLORS_AND_ICON =
+            new CarTextConstraints(Arrays.asList(
+                    DistanceSpan.class,
+                    DurationSpan.class,
+                    ForegroundCarColorSpan.class,
+                    CarIconSpan.class));
+
     private final HashSet<Class<? extends CarSpan>> mAllowedTypes;
 
     /**
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
index 9a9bd33..49c96d4 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppNotificationBroadcastReceiver.java
@@ -47,6 +47,7 @@
 public class CarAppNotificationBroadcastReceiver extends BroadcastReceiver {
     private static final String TAG = LogTags.TAG + ".NBR";
 
+    @SuppressWarnings("deprecation")
     @Override
     public void onReceive(@NonNull Context context, @NonNull Intent intent) {
         ComponentName appComponent =
diff --git a/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java b/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
index 5a03bb1..86daab0 100644
--- a/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/notification/CarPendingIntentTest.java
@@ -52,6 +52,7 @@
     private final ComponentName mComponentName = new ComponentName(mContext, CarAppService.class);
     private final Intent mIntent = new Intent("fooAction").setComponent(mComponentName);
 
+    @SuppressWarnings("deprecation")
     @Test
     public void getCarApp_returnsTheExpectedPendingIntent() throws PendingIntent.CanceledException {
         PendingIntent pendingIntent = CarPendingIntent.getCarApp(mContext, 1, mIntent,
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index e88b101..2adb329 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -43,7 +43,7 @@
         implementation("androidx.compose.runtime:runtime-saveable:1.1.0-rc01")
         implementation(project(":compose:ui:ui-util"))
         implementation(libs.kotlinStdlib)
-        implementation("androidx.core:core:1.5.0")
+        implementation("androidx.core:core:1.7.0")
         implementation('androidx.collection:collection:1.0.0')
 
         testImplementation(project(":compose:ui:ui-test-font"))
@@ -121,7 +121,7 @@
 
             androidMain.dependencies {
                 api("androidx.annotation:annotation:1.1.0")
-                implementation("androidx.core:core:1.5.0")
+                implementation("androidx.core:core:1.7.0")
                 implementation('androidx.collection:collection:1.0.0')
             }
 
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
index f85edaa..aed16bd 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/ObjectAnimatorTest.java
@@ -312,7 +312,7 @@
         String propertyName = "backgroundColor";
         int startColor = Color.RED;
         int endColor = Color.BLUE;
-        Object[] values = {new Integer(startColor), new Integer(endColor)};
+        Object[] values = {startColor, endColor};
         ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
         ObjectAnimator colorAnimator = ObjectAnimator.ofObject(object, propertyName,
                 evaluator, values);
@@ -360,7 +360,7 @@
         String propertyName = "backgroundColor";
         int startColor = Color.RED;
         int endColor = Color.BLUE;
-        Object[] values = {new Integer(startColor), new Integer(endColor)};
+        Object[] values = {startColor, endColor};
         ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
         ObjectAnimator colorAnimator = ObjectAnimator.ofObject(object, propertyName,
                 evaluator, values);
diff --git a/core/core-animation/build.gradle b/core/core-animation/build.gradle
index 0b22cb9..45da746 100644
--- a/core/core-animation/build.gradle
+++ b/core/core-animation/build.gradle
@@ -25,6 +25,7 @@
     api("androidx.annotation:annotation:1.2.0")
     implementation("androidx.core:core:1.3.1")
     implementation("androidx.collection:collection:1.1.0")
+    implementation("androidx.tracing:tracing:1.0.0")
 
     androidTestImplementation(libs.testExtJunit, excludes.espresso)
     androidTestImplementation(libs.testRules, excludes.espresso)
diff --git a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
index 35db26b..4654d57 100644
--- a/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
+++ b/core/core-animation/src/main/java/androidx/core/animation/ValueAnimator.java
@@ -27,7 +27,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.core.os.TraceCompat;
+import androidx.tracing.Trace;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -1164,7 +1164,7 @@
         }
         // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
         mReversing = false;
-        TraceCompat.endSection();
+        Trace.endSection();
     }
 
     /**
@@ -1173,7 +1173,7 @@
      */
     private void startAnimation() {
 
-        TraceCompat.beginSection(getNameForTrace());
+        Trace.beginSection(getNameForTrace());
 
         mAnimationEndRequested = false;
         initAnimation();
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 d712487..fd87143 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
@@ -662,6 +662,7 @@
     @SdkSuppress(minSdkVersion = 29)
     @LargeTest
     @Test
+    @SuppressWarnings("deprecation")
     public void testFixedAllFileChecksumsSingleThread() throws Exception {
         installPackage(TEST_FIXED_APK);
         assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
@@ -951,6 +952,7 @@
                 trustedInstallers);
     }
 
+    @SuppressWarnings("deprecation")
     private Checksum[] getFileChecksums(@NonNull String packageName,
             @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
             throws Exception {
@@ -1184,6 +1186,7 @@
             }
         }
 
+        @SuppressWarnings("deprecation")
         static Certificate getInstallerCertificate(Context context) throws Exception {
             PackageManager pm = context.getPackageManager();
             PackageInfo installerPackageInfo = pm.getPackageInfo(INSTALLER_PACKAGE_NAME,
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 d739e9b..122e0ab 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
@@ -72,7 +72,9 @@
     /**
      * The name of the dialer role.
      * <p>
-     * To qualify for this role, an application needs to handle the intent to dial:
+     * To qualify for this role, an application needs to handle the intent to dial, and implement
+     * an {@link android.telecom.InCallService} if the application targets
+     * {@link android.os.Build.VERSION_CODES.TIRAMISU} or higher:
      * <pre class="prettyprint">{@code
      * <activity>
      *     <intent-filter>
@@ -85,6 +87,16 @@
      *         <data android:scheme="tel" />
      *     </intent-filter>
      * </activity>
+     * <service android:permission="android.permission.BIND_INCALL_SERVICE">
+     *     <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
+     *     <meta-data
+     *         android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI"
+     *         android:value="false" />
+     *     <intent-filter>
+     *         <action android:name="android.telecom.InCallService" />
+     *     </intent-filter>
+     * </service>
+     *
      * }</pre>
      * The application will be able to handle those intents by default, and gain access to phone,
      * contacts, SMS, microphone and camera.
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index ac52137..83fef5e 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1010,10 +1010,15 @@
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
     method public static boolean startActivities(android.content.Context, android.content.Intent![]);
     method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
     method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
     method public static void startForegroundService(android.content.Context, android.content.Intent);
+    field public static final int RECEIVER_EXPORTED = 2; // 0x2
+    field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+    field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
   }
 
   public class FileProvider extends android.content.ContentProvider {
@@ -1735,6 +1740,7 @@
     method public java.util.Locale? getFirstMatch(String![]);
     method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
     method public boolean isEmpty();
+    method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
     method @IntRange(from=0) public int size();
     method public String toLanguageTags();
     method public Object? unwrap();
@@ -1753,7 +1759,18 @@
   }
 
   public final class ParcelCompat {
+    method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static boolean readBoolean(android.os.Parcel);
+    method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+    method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+    method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+    method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+    method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static void writeBoolean(android.os.Parcel, boolean);
   }
 
@@ -3022,6 +3039,9 @@
     method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
     method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
     field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
     field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
     field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
     field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3118,6 +3138,7 @@
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+    method public String? getUniqueId();
     method public String! getViewIdResourceName();
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
     method public int getWindowId();
@@ -3211,6 +3232,7 @@
     method public void setTraversalAfter(android.view.View!, int);
     method public void setTraversalBefore(android.view.View!);
     method public void setTraversalBefore(android.view.View!, int);
+    method public void setUniqueId(String?);
     method public void setViewIdResourceName(String!);
     method public void setVisibleToUser(boolean);
     method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3272,6 +3294,9 @@
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index abd1c17..70ec372 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1010,10 +1010,15 @@
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
     method public static boolean startActivities(android.content.Context, android.content.Intent![]);
     method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
     method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
     method public static void startForegroundService(android.content.Context, android.content.Intent);
+    field public static final int RECEIVER_EXPORTED = 2; // 0x2
+    field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+    field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
   }
 
   public class FileProvider extends android.content.ContentProvider {
@@ -1691,7 +1696,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
     method @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
-    method @ChecksSdkIntAtLeast(codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
+    method @ChecksSdkIntAtLeast(api=33) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
   }
 
   @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public static @interface BuildCompat.PrereleaseSdkCheck {
@@ -1740,6 +1745,7 @@
     method public java.util.Locale? getFirstMatch(String![]);
     method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
     method public boolean isEmpty();
+    method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
     method @IntRange(from=0) public int size();
     method public String toLanguageTags();
     method public Object? unwrap();
@@ -1758,7 +1764,18 @@
   }
 
   public final class ParcelCompat {
+    method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static boolean readBoolean(android.os.Parcel);
+    method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+    method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+    method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+    method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+    method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static void writeBoolean(android.os.Parcel, boolean);
   }
 
@@ -3027,6 +3044,9 @@
     method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
     method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
     field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
     field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
     field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
     field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3123,6 +3143,7 @@
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+    method public String? getUniqueId();
     method public String! getViewIdResourceName();
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
     method public int getWindowId();
@@ -3216,6 +3237,7 @@
     method public void setTraversalAfter(android.view.View!, int);
     method public void setTraversalBefore(android.view.View!);
     method public void setTraversalBefore(android.view.View!, int);
+    method public void setUniqueId(String?);
     method public void setViewIdResourceName(String!);
     method public void setVisibleToUser(boolean);
     method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3277,6 +3299,9 @@
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index ec250b5..12ad90c 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1118,10 +1118,15 @@
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, int);
+    method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
     method public static boolean startActivities(android.content.Context, android.content.Intent![]);
     method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
     method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
     method public static void startForegroundService(android.content.Context, android.content.Intent);
+    field public static final int RECEIVER_EXPORTED = 2; // 0x2
+    field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
+    field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
   }
 
   public class FileProvider extends android.content.ContentProvider {
@@ -2069,6 +2074,7 @@
     method public java.util.Locale? getFirstMatch(String![]);
     method @IntRange(from=0xffffffff) public int indexOf(java.util.Locale?);
     method public boolean isEmpty();
+    method @RequiresApi(21) public static boolean matchesLanguageAndScript(java.util.Locale, java.util.Locale);
     method @IntRange(from=0) public int size();
     method public String toLanguageTags();
     method public Object? unwrap();
@@ -2087,7 +2093,18 @@
   }
 
   public final class ParcelCompat {
+    method public static <T> T![]? readArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> java.util.ArrayList<T!>? readArrayList(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static boolean readBoolean(android.os.Parcel);
+    method public static <K, V> java.util.HashMap<K!,V!>? readHashMap(android.os.Parcel, ClassLoader?, Class<? extends K>, Class<? extends V>);
+    method public static <T> void readList(android.os.Parcel, java.util.List<? super T>, ClassLoader?, Class<T!>);
+    method public static <K, V> void readMap(android.os.Parcel, java.util.Map<? super K,? super V>, ClassLoader?, Class<K!>, Class<V!>);
+    method public static <T extends android.os.Parcelable> T? readParcelable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> T![]? readParcelableArray(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(30) public static <T> android.os.Parcelable.Creator<T!>? readParcelableCreator(android.os.Parcel, ClassLoader?, Class<T!>);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
+    method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
+    method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T>);
     method public static void writeBoolean(android.os.Parcel, boolean);
   }
 
@@ -3478,6 +3495,9 @@
     method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType int);
     method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
     field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 256; // 0x100
+    field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
     field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
     field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
     field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -3506,7 +3526,7 @@
     field @Deprecated public static final int TYPE_WINDOW_CONTENT_CHANGED = 2048; // 0x800
   }
 
-  @IntDef(flag=true, value={androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_TEXT, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface AccessibilityEventCompat.ContentChangeType {
+  @IntDef(flag=true, value={androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_TEXT, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_STARTED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_DROPPED, androidx.core.view.accessibility.AccessibilityEventCompat.CONTENT_CHANGE_TYPE_DRAG_CANCELLED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface AccessibilityEventCompat.ContentChangeType {
   }
 
   public final class AccessibilityManagerCompat {
@@ -3579,6 +3599,7 @@
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat.TouchDelegateInfoCompat? getTouchDelegateInfo();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalAfter();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat! getTraversalBefore();
+    method public String? getUniqueId();
     method public String! getViewIdResourceName();
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat! getWindow();
     method public int getWindowId();
@@ -3672,6 +3693,7 @@
     method public void setTraversalAfter(android.view.View!, int);
     method public void setTraversalBefore(android.view.View!);
     method public void setTraversalBefore(android.view.View!, int);
+    method public void setUniqueId(String?);
     method public void setViewIdResourceName(String!);
     method public void setVisibleToUser(boolean);
     method public android.view.accessibility.AccessibilityNodeInfo! unwrap();
@@ -3737,6 +3759,9 @@
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_COPY;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_CUT;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_DISMISS;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_CANCEL;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_DROP;
+    field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat ACTION_DRAG_START;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_EXPAND;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_FOCUS;
     field public static final androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat! ACTION_HIDE_TOOLTIP;
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index ae9dd30..ad0606f 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -1247,28 +1247,6 @@
     <issue
         id="BanUncheckedReflection"
         message="Calling `Method.invoke` without an SDK check"
-        errorLine1="                    return (IBinder) sGetIBinderMethod.invoke(bundle, key);"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/BundleCompat.java"
-            line="60"
-            column="38"/>
-    </issue>
-
-    <issue
-        id="BanUncheckedReflection"
-        message="Calling `Method.invoke` without an SDK check"
-        errorLine1="                    sPutIBinderMethod.invoke(bundle, key, binder);"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/BundleCompat.java"
-            line="84"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="BanUncheckedReflection"
-        message="Calling `Method.invoke` without an SDK check"
         errorLine1="            return (String) getMethod.invoke(systemProperties, name);"
         errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -3105,259 +3083,6 @@
 
     <issue
         id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeCustomAnimation(context,"
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="69"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeScaleUpAnimation("
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="99"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeClipRevealAnimation("
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="123"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeThumbnailScaleUpAnimation("
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="152"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeSceneTransitionAnimation("
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="180"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                    ActivityOptions.makeSceneTransitionAnimation(activity, pairs));"
-        errorLine2="                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="217"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 21; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeTaskLaunchBehind());"
-        errorLine2="                                                                 ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="235"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return new ActivityOptionsCompatImpl(ActivityOptions.makeBasic());"
-        errorLine2="                                                                 ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="247"
-            column="66"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                mActivityOptions.requestUsageTimeReport(receiver);"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="277"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 24; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                    mActivityOptions.setLaunchBounds(screenSpacePixelRect));"
-        errorLine2="                                     ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="288"
-            column="38"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 24; however, the containing class androidx.core.app.ActivityOptionsCompat.ActivityOptionsCompatImpl is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return mActivityOptions.getLaunchBounds();"
-        errorLine2="                                    ~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/ActivityOptionsCompat.java"
-            line="296"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 21; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, showIntent),"
-        errorLine2="                         ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="62"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 21; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, showIntent),"
-        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="62"
-            column="40"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="120"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 19; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            alarmManager.setExact(type, triggerAtMillis, operation);"
-        errorLine2="                         ~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="165"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AlarmManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation);"
-        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AlarmManagerCompat.java"
-            line="225"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return AppOpsManager.permissionToOp(permission);"
-        errorLine2="                                 ~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="79"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 19; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return appOpsManager.noteOp(op, uid, packageName);"
-        errorLine2="                                 ~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="110"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 19; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return appOpsManager.noteOpNoThrow(op, uid, packageName);"
-        errorLine2="                                 ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="130"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="160"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return appOpsManager.noteProxyOp(op, proxiedPackageName);"
-        errorLine2="                                 ~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="161"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="179"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 23; however, the containing class androidx.core.app.AppOpsManagerCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return appOpsManager.noteProxyOpNoThrow(op, proxiedPackageName);"
-        errorLine2="                                 ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/AppOpsManagerCompat.java"
-            line="180"
-            column="34"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
         message="This call references a method added in API level 17; however, the containing class androidx.core.graphics.BitmapCompat is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="            return bitmap.hasMipMap();"
         errorLine2="                          ~~~~~~~~~">
@@ -3402,28 +3127,6 @@
 
     <issue
         id="ClassVerificationFailure"
-        message="This call references a method added in API level 18; however, the containing class androidx.core.app.BundleCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return bundle.getBinder(key);"
-        errorLine2="                          ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/BundleCompat.java"
-            line="106"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 18; however, the containing class androidx.core.app.BundleCompat is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            bundle.putBinder(key, binder);"
-        errorLine2="                   ~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/BundleCompat.java"
-            line="122"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
         message="This call references a method added in API level 16; however, the containing class androidx.core.os.CancellationSignal is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="                ((android.os.CancellationSignal) obj).cancel();"
         errorLine2="                                                      ~~~~~~">
@@ -3490,6 +3193,215 @@
 
     <issue
         id="ClassVerificationFailure"
+        message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                    .getDisplay(displayId);"
+        errorLine2="                     ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+            line="86"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                    .getDisplays();"
+        errorLine2="                     ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+            line="106"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 17; however, the containing class androidx.core.hardware.display.DisplayManagerCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                    .getDisplays(category);"
+        errorLine2="                     ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/hardware/display/DisplayManagerCompat.java"
+            line="135"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setAutoMirrored(mirrored);"
+        errorLine2="                     ~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="79"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.isAutoMirrored();"
+        errorLine2="                            ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="96"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setHotspot(x, y);"
+        errorLine2="                     ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="111"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setHotspotBounds(left, top, right, bottom);"
+        errorLine2="                     ~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="124"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setTint(tint);"
+        errorLine2="                     ~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="136"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setTintList(tint);"
+        errorLine2="                     ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="150"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.setTintMode(tintMode);"
+        errorLine2="                     ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="164"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.getAlpha();"
+        errorLine2="                            ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="178"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.applyTheme(theme);"
+        errorLine2="                     ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="189"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.canApplyTheme();"
+        errorLine2="                            ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="198"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.getColorFilter();"
+        errorLine2="                            ~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="211"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                clearColorFilter(((InsetDrawable) drawable).getDrawable());"
+        errorLine2="                                                            ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="232"
+            column="61"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 19; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="                        child = state.getChild(i);"
+        errorLine2="                                      ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="242"
+            column="39"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 21; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            drawable.inflate(res, parser, attrs, theme);"
+        errorLine2="                     ~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="269"
+            column="22"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 23; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.setLayoutDirection(layoutDirection);"
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="355"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
+        message="This call references a method added in API level 23; however, the containing class androidx.core.graphics.drawable.DrawableCompat is reachable from earlier API levels and will fail run-time class verification."
+        errorLine1="            return drawable.getLayoutDirection();"
+        errorLine2="                            ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/graphics/drawable/DrawableCompat.java"
+            line="392"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="ClassVerificationFailure"
         message="This call references a method added in API level 28; however, the containing class androidx.core.app.DialogCompat is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="            return dialog.requireViewById(id);"
         errorLine2="                          ~~~~~~~~~~~~~~~">
@@ -3622,39 +3534,6 @@
 
     <issue
         id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            return sourceActivity.shouldUpRecreateTask(targetIntent);"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/NavUtils.java"
-            line="61"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            sourceActivity.navigateUpTo(upIntent);"
-        errorLine2="                           ~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/NavUtils.java"
-            line="109"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 16; however, the containing class androidx.core.app.NavUtils is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="            Intent result = sourceActivity.getParentActivityIntent();"
-        errorLine2="                                           ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/core/app/NavUtils.java"
-            line="132"
-            column="44"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
         message="This call references a method added in API level 26; however, the containing class androidx.core.app.NotificationChannelCompat is reachable from earlier API levels and will fail run-time class verification."
         errorLine1="        this(channel.getId(), channel.getImportance());"
         errorLine2="                     ~~~~~">
diff --git a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
index 9800531..1968a78 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ActivityCompatTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.eq;
@@ -31,7 +32,9 @@
 import android.support.v4.BaseInstrumentationTestCase;
 import android.view.View;
 
+import androidx.annotation.OptIn;
 import androidx.core.app.ActivityCompat.PermissionCompatDelegate;
+import androidx.core.os.BuildCompat;
 import androidx.core.test.R;
 import androidx.test.core.app.ActivityScenario;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -145,4 +148,13 @@
         ActivityCompat.requireViewById(getActivity(), View.NO_ID);
     }
 
+    @Test
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public void testShouldShowRequestPermissionRationaleForPostNotifications() throws Throwable {
+        if (!BuildCompat.isAtLeastT()) {
+            // permission doesn't exist yet, so should return false
+            assertFalse(ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),
+                    Manifest.permission.POST_NOTIFICATIONS));
+        }
+    }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java b/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
index 9d5b663..3119729 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ComponentActivityTest.java
@@ -23,6 +23,7 @@
 import android.os.Build;
 import android.support.v4.BaseInstrumentationTestCase;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
@@ -90,6 +91,16 @@
         shouldNotDumpSpecialArgOnVersion("--translation", Build.VERSION_CODES.S);
     }
 
+    @Test
+    public void testShouldDumpInternalState_listDumpables() {
+        shouldNotDumpSpecialArgOnT("--list-dumpables");
+    }
+
+    @Test
+    public void testShouldDumpInternalState_dumpDumpable() {
+        shouldNotDumpSpecialArgOnT("--dump-dumpable");
+    }
+
     private void shouldNotDumpSpecialArgOnVersion(String specialArg, int minApiVersion) {
         String[] args = { specialArg };
         int actualApiVersion = Build.VERSION.SDK_INT;
@@ -103,6 +114,19 @@
         }
     }
 
+    private void shouldNotDumpSpecialArgOnT(String specialArg) {
+        String[] args = { specialArg };
+        int actualApiVersion = Build.VERSION.SDK_INT;
+
+        if (BuildCompat.isAtLeastT()) {
+            assertFalse(specialArg + " should be skipped on API " + actualApiVersion,
+                    mComponentActivity.shouldDumpInternalState(args));
+        } else {
+            assertTrue(specialArg + " should be ignored on API " + actualApiVersion,
+                    mComponentActivity.shouldDumpInternalState(args));
+        }
+    }
+
     private class NeverAddedExtraData extends ComponentActivity.ExtraData {
     }
 
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index af95506..ed0ed4d 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -264,6 +264,7 @@
             Log.i(TAG, "Running!");
         }
 
+        @SuppressWarnings("deprecation")
         @Override
         protected void onHandleWork(@NonNull Intent intent) {
             Log.i(TAG, "Handling work: " + intent);
diff --git a/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
index 0acf64e..90f0788 100644
--- a/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/ShareCompatTest.java
@@ -38,7 +38,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-
+@SuppressWarnings("deprecation")
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class ShareCompatTest extends BaseInstrumentationTestCase<TestActivity> {
@@ -54,6 +54,7 @@
         super(TestActivity.class);
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void testBuilder() {
         Activity activity = mActivityTestRule.getActivity();
@@ -71,6 +72,7 @@
                 activity.getComponentName());
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void testBuilderWithoutActivity() {
         Context context = mActivityTestRule.getActivity().getApplicationContext();
@@ -86,6 +88,7 @@
         assertNull(intent.getParcelableExtra(ShareCompat.EXTRA_CALLING_ACTIVITY_INTEROP));
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     public void testBuilderWithWrappedActivity() {
         Activity activity = mActivityTestRule.getActivity();
@@ -104,6 +107,7 @@
                 activity.getComponentName());
     }
 
+    @SuppressWarnings("deprecation")
     @Test
     @SdkSuppress(minSdkVersion = 16)
     public void testBuilderSingleStreamUri() {
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index e55d822..afb8249 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -70,8 +70,14 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
+import android.Manifest;
 import android.accounts.AccountManager;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
@@ -87,9 +93,12 @@
 import android.app.usage.UsageStatsManager;
 import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.RestrictionsManager;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
@@ -134,6 +143,9 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.textservice.TextServicesManager;
 
+import androidx.annotation.OptIn;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.os.BuildCompat;
 import androidx.core.test.R;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
@@ -146,7 +158,14 @@
 @LargeTest
 public class ContextCompatTest extends BaseInstrumentationTestCase<ThemedYellowActivity> {
     private Context mContext;
+    private IntentFilter mTestFilter = new IntentFilter();
+    private String mPermission;
+    private BroadcastReceiver mTestReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
 
+        }
+    };
     public ContextCompatTest() {
         super(ThemedYellowActivity.class);
     }
@@ -154,6 +173,7 @@
     @Before
     public void setup() {
         mContext = mActivityTestRule.getActivity();
+        mPermission = mContext.getPackageName() + ".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
     }
 
     @Test
@@ -445,6 +465,71 @@
         return ((size * tdensity) + (sdensity >> 1)) / sdensity;
     }
 
+    @Test
+    public void testRegisterReceiver_noExportStateFlagThrowsException() {
+        assertThrows(IllegalArgumentException.class, () -> ContextCompat.registerReceiver(mContext,
+                mTestReceiver, mTestFilter, 0));
+
+        assertThrows(IllegalArgumentException.class, () -> ContextCompat.registerReceiver(mContext,
+                mTestReceiver, mTestFilter, Context.RECEIVER_VISIBLE_TO_INSTANT_APPS));
+    }
+
+    @Test
+    public void testRegisterReceiver_specifyBothExportStateFlagsThrowsException() {
+        assertThrows(IllegalArgumentException.class,
+                () -> ContextCompat.registerReceiver(mContext,
+                mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_EXPORTED | ContextCompat.RECEIVER_NOT_EXPORTED));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testRegisterReceiverApi33() {
+        Context spyContext = spy(mContext);
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null),
+                any(), eq(ContextCompat.RECEIVER_NOT_EXPORTED));
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any(),
+                eq(ContextCompat.RECEIVER_EXPORTED));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26, maxSdkVersion = 32)
+    public void testRegisterReceiverApi26() {
+        Context spyContext = spy(mContext);
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter),
+                eq(mPermission), any());
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any(),
+                eq(0));
+
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 25)
+    public void testRegisterReceiver() {
+        Context spyContext = spy(mContext);
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_NOT_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(mPermission),
+                any());
+
+        ContextCompat.registerReceiver(spyContext, mTestReceiver, mTestFilter,
+                ContextCompat.RECEIVER_EXPORTED);
+        verify(spyContext).registerReceiver(eq(mTestReceiver), eq(mTestFilter), eq(null), any());
+    }
+
     @Test(expected = NullPointerException.class)
     public void testCheckSelfPermissionNull() {
         ContextCompat.checkSelfPermission(mContext, null);
@@ -474,4 +559,22 @@
                 ContextCompat.checkSelfPermission(mContext,
                         android.Manifest.permission.DELETE_PACKAGES));
     }
+
+    @Test
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public void testCheckSelfPermissionNotificationPermission() {
+        if (BuildCompat.isAtLeastT()) {
+            assertEquals(
+                    mContext.checkCallingPermission(Manifest.permission.POST_NOTIFICATIONS),
+                    ContextCompat.checkSelfPermission(
+                            mContext,
+                            Manifest.permission.POST_NOTIFICATIONS));
+        } else {
+            assertEquals("Notification permission allowed by default on devices <= SDK 32",
+                    NotificationManagerCompat.from(mContext).areNotificationsEnabled()
+                            ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED,
+                    ContextCompat.checkSelfPermission(mContext,
+                            Manifest.permission.POST_NOTIFICATIONS));
+        }
+    }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
index 98a9aea..d612ba4 100644
--- a/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
@@ -318,6 +318,7 @@
      * this case, they are permission revocation apps.
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    @SuppressWarnings("deprecation")
     static void setupPermissionRevocationApps(
             PackageManager packageManager, List<String> packageNames) {
         List<ResolveInfo> resolveInfos = new ArrayList<>();
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
index 53f5500..bc98c5f 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
@@ -333,6 +333,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     private fun mockPackageManager() = mockThrowOnUnmocked<PackageManager> {
         val mockCerts = params.mockCerts
         whenever(getPackageInfo(TEST_PKG_NAME, params.mockCerts.flag)) {
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
index b4eb56a..6bab3ca 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/ShortcutManagerCompatTest.java
@@ -89,7 +89,7 @@
 import java.util.Collections;
 import java.util.List;
 
-@SuppressWarnings("unchecked")
+@SuppressWarnings({"unchecked", "deprecation"})
 @RunWith(AndroidJUnit4.class)
 public class ShortcutManagerCompatTest extends BaseInstrumentationTestCase<TestActivity> {
 
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
index 0ee4d9a..6efc56f 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/TypefaceCompatTest.java
@@ -59,6 +59,7 @@
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
+@SuppressWarnings("deprecation")
 public class TypefaceCompatTest {
 
     public Context mContext;
diff --git a/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java b/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
index 28d9898..60d3c11 100644
--- a/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
@@ -57,6 +57,7 @@
 import java.io.OutputStream;
 import java.util.Arrays;
 
+@SuppressWarnings("deprecation")
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class IconCompatTest {
diff --git a/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
index 3fd5f6d..6bd9800 100644
--- a/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/LocaleListCompatTest.java
@@ -393,6 +393,44 @@
         assertNotEquals(first.toLanguageTags(), second.toLanguageTags());
     }
 
+    @SdkSuppress(minSdkVersion = 21)
+    @Test
+    public void testLocaleListCompat_matchesLanguageAndScript() {
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("fr-Latn-FR"),
+                forLanguageTag("fr-Latn")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hans-CN"),
+                forLanguageTag("zh-Hans")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hant-TW"),
+                forLanguageTag("zh-Hant")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+                forLanguageTag("en-US")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+                forLanguageTag("en-CA")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("ar-NA"),
+                forLanguageTag("ar-ZA")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+                forLanguageTag("zh")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+                forLanguageTag("zh-Hans")));
+        assertTrue(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+                forLanguageTag("zh-Hant")));
+
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-Hant-TW"),
+                forLanguageTag("zh-Hans")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-XA"),
+                forLanguageTag("en-US")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("ar-YE"),
+                forLanguageTag("ar-XB")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("en-US"),
+                forLanguageTag("zh-TW")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+                forLanguageTag("zh")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-CN"),
+                forLanguageTag("zh-Hant")));
+        assertFalse(LocaleListCompat.matchesLanguageAndScript(forLanguageTag("zh-TW"),
+                forLanguageTag("zh-Hans")));
+    }
+
     private Locale forLanguageTag(String str) {
         if (Build.VERSION.SDK_INT >= 21) {
             return Locale.forLanguageTag(str);
diff --git a/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
index e1d273b..873a9c2 100644
--- a/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/os/ParcelCompatTest.java
@@ -16,17 +16,31 @@
 
 package androidx.core.os;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import android.content.pm.Signature;
+import android.graphics.Rect;
+import android.os.Build;
 import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
 
+import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Objects;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ParcelCompatTest {
@@ -40,4 +54,186 @@
         assertTrue(ParcelCompat.readBoolean(p));
         assertFalse(ParcelCompat.readBoolean(p));
     }
+
+    @Test
+    public void readParcelable2Arg() {
+        Rect r = new Rect(0, 0, 10, 10);
+        Parcel p = Parcel.obtain();
+        p.writeParcelable(r, 0);
+
+        p.setDataPosition(0);
+        Rect r2 = ParcelCompat.readParcelable(p, Rect.class.getClassLoader(), Rect.class);
+        assertEquals(r, r2);
+    }
+
+    @Test
+    public void readArrayInT() {
+        Parcel p = Parcel.obtain();
+
+        Signature[] s = {new Signature("1234"),
+                null,
+                new Signature("abcd")};
+        p.writeArray(s);
+
+        p.setDataPosition(0);
+        Object[] objects = ParcelCompat.readArray(p, Signature.class.getClassLoader(),
+                Signature.class);
+        assertTrue(Arrays.equals(s, objects));
+        p.setDataPosition(0);
+
+        p.recycle();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.S)
+    @Test
+    public void readSparseArrayInT() {
+        Parcel p = Parcel.obtain();
+
+        SparseArray<Signature> s = new SparseArray<>();
+        s.put(0, new Signature("1234567890abcdef"));
+        s.put(2, null);
+        s.put(3, new Signature("abcdef1234567890"));
+        p.writeSparseArray(s);
+
+        p.setDataPosition(0);
+        SparseArray<Signature> s1 = ParcelCompat.readSparseArray(p,
+                Signature.class.getClassLoader(), Signature.class);
+        assertEquals(s.size(), s1.size());
+        for (int index = 0; index < s.size(); index++) {
+            int key = s.keyAt(index);
+            assertTrue(Objects.equals(s.valueAt(index), s1.get(key)));
+        }
+
+        p.recycle();
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void readListInT() {
+        Parcel p = Parcel.obtain();
+        ArrayList<Signature> s = new ArrayList();
+        ArrayList<Signature> s2 = new ArrayList();
+        s.add(new Signature("1234567890abcdef"));
+        s.add(new Signature("abcdef1234567890"));
+
+        p.writeList(s);
+        p.setDataPosition(0);
+        ParcelCompat.readList(p, s2, Signature.class.getClassLoader(), Signature.class);
+        assertEquals(2, s2.size());
+        for (int i = 0; i < s2.size(); i++) {
+            assertEquals(s.get(i), s2.get(i));
+        }
+        p.recycle();
+    }
+
+    @Test
+    public void readArrayListInT() {
+        Parcel p = Parcel.obtain();
+
+        ArrayList<Signature> s = new ArrayList<>();
+        s.add(new Signature("1234567890abcdef"));
+        s.add(null);
+        s.add(new Signature("abcdef1234567890"));
+
+        p.writeList(s);
+        p.setDataPosition(0);
+        ArrayList<Signature> s1 = ParcelCompat.readArrayList(p, Signature.class.getClassLoader(),
+                Signature.class);
+        assertEquals(s, s1);
+
+        p.recycle();
+    }
+
+    @Test
+    public void readMapInT() {
+        Parcel p = Parcel.obtain();
+        ClassLoader loader = getClass().getClassLoader();
+        HashMap<String, Signature> map = new HashMap<>();
+        HashMap<String, Signature> map2 = new HashMap<>();
+
+        map.put("key1", new Signature("abcd"));
+        map.put("key2", new Signature("ABCD"));
+        p.writeMap(map);
+        p.setDataPosition(0);
+        ParcelCompat.readMap(p, map2, Signature.class.getClassLoader(), String.class,
+                Signature.class);
+        assertEquals(map, map2);
+
+        p.recycle();
+    }
+
+    @Test
+    public void readHashMapInT() {
+        Parcel p = Parcel.obtain();
+        ClassLoader loader = getClass().getClassLoader();
+        HashMap<String, Signature> map = new HashMap<>();
+        HashMap<String, Signature> map2 = new HashMap<>();
+
+        map.put("key1", new Signature("abcd"));
+        map.put("key2", new Signature("ABCD"));
+        p.writeMap(map);
+        p.setDataPosition(0);
+        map2 = ParcelCompat.readHashMap(p, loader, String.class, Signature.class);
+        assertEquals(map, map2);
+
+        p.recycle();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    public void readParcelableCreatorInT() {
+        final String signatureString  = "1234567890abcdef";
+        Signature s = new Signature(signatureString);
+
+        Parcel p = Parcel.obtain();
+        p.writeParcelableCreator(s);
+        p.setDataPosition(0);
+        assertSame(Signature.CREATOR, ParcelCompat.readParcelableCreator(p,
+                Signature.class.getClassLoader(), Signature.class));
+
+        p.setDataPosition(0);
+        p.recycle();
+    }
+
+    @Test
+    public void readParcelableArrayInT() {
+        Parcel p = Parcel.obtain();
+        Signature[] s = {new Signature("1234"),
+                null,
+                new Signature("abcd")
+        };
+        p.writeParcelableArray(s, 0);
+        p.setDataPosition(0);
+        Parcelable[] s1 = ParcelCompat.readParcelableArray(p, Signature.class.getClassLoader(),
+                Signature.class);
+        assertTrue(Arrays.equals(s, s1));
+        p.recycle();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    public void readParcelableListInT() {
+        final Parcel p = Parcel.obtain();
+        ArrayList<Signature> list = new ArrayList<>();
+        ArrayList<Signature> list1 = new ArrayList<>();
+        list.add(new Signature("1234"));
+        list.add(new Signature("4321"));
+        p.writeParcelableList(list, 0);
+        p.setDataPosition(0);
+        ParcelCompat.readParcelableList(p, list1, Signature.class.getClassLoader(),
+                Signature.class);
+        assertEquals(list, list1);
+        p.recycle();
+    }
+
+    @Test
+    public void readSerializable2Arg() {
+        String s = "Hello World";
+        Parcel p = Parcel.obtain();
+        p.writeSerializable(s);
+
+        p.setDataPosition(0);
+        String s2 = ParcelCompat.readSerializable(p, String.class.getClassLoader(), String.class);
+        assertEquals(s, s2);
+    }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
index 4499af4..5112fed 100644
--- a/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/provider/FontsContractCompatTest.java
@@ -68,6 +68,7 @@
  */
 @RunWith(AndroidJUnit4.class)
 @MediumTest
+@SuppressWarnings("deprecation")
 public class FontsContractCompatTest {
     private static final String AUTHORITY = "androidx.core.provider.fonts.font";
     private static final String PACKAGE = "androidx.core.test";
@@ -322,6 +323,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void testGetProvider_duplicateCerts()
             throws PackageManager.NameNotFoundException {
         PackageManager packageManager = mock(PackageManager.class);
@@ -377,6 +379,7 @@
         }
     }
 
+    @SuppressWarnings("deprecation")
     private ProviderInfo setupPackageManager(PackageManager packageManager)
             throws PackageManager.NameNotFoundException {
         ProviderInfo info = new ProviderInfo();
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index e32d604..275e727 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -125,6 +125,14 @@
         assertThat(nodeCompat.isTextEntryKey(), is(false));
     }
 
+    @Test
+    public void testGetSetUniqueId() {
+        final String uniqueId = (Build.VERSION.SDK_INT >= 19) ? "localUId" : null;
+        AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
+        nodeCompat.setUniqueId(uniqueId);
+        assertThat(nodeCompat.getUniqueId(), equalTo(uniqueId));
+    }
+
     @SdkSuppress(minSdkVersion = 19)
     @Test
     public void testAccessibilityActionsNotNull() {
diff --git a/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
index 2a9a40b..0765571 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/TextViewCompatTest.java
@@ -489,6 +489,7 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 26, maxSdkVersion =  27)
+    @SuppressWarnings("deprecation")
     public void testSetCustomSelectionActionModeCallback_fixesBugInO() {
         // Create mock context and package manager for the text view.
         final PackageManager packageManagerMock = spy(mTextView.getContext().getPackageManager());
diff --git a/core/core/src/main/AndroidManifest.xml b/core/core/src/main/AndroidManifest.xml
index 8481017..b050c7e 100644
--- a/core/core/src/main/AndroidManifest.xml
+++ b/core/core/src/main/AndroidManifest.xml
@@ -16,4 +16,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <application
         android:appComponentFactory="androidx.core.app.CoreComponentFactory" />
+    <permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
+    <uses-permission android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
 </manifest>
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 9cd1859..48bd8fe 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityCompat.java
@@ -16,6 +16,7 @@
 
 package androidx.core.app;
 
+import android.Manifest;
 import android.app.Activity;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -40,15 +41,19 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 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;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Helper for accessing features in {@link android.app.Activity}.
@@ -331,6 +336,7 @@
      * <p>Note that this is <em>not</em> a security feature -- you can not trust the
      * referrer information, applications can spoof it.</p>
      */
+    @SuppressWarnings("deprecation")
     @Nullable
     public static Uri getReferrer(@NonNull Activity activity) {
         if (Build.VERSION.SDK_INT >= 22) {
@@ -486,6 +492,15 @@
      * RuntimePermissions</a> sample app demonstrates how to use this method to
      * request permissions at run time.
      * </p>
+     * <p>
+     * If {@link Manifest.permission#POST_NOTIFICATIONS} is requested before the device supports
+     * the notification permission, then {@link Manifest.permission#POST_NOTIFICATIONS} will be
+     * removed from {@link OnRequestPermissionsResultCallback#onRequestPermissionsResult}.
+     * For devices that don't support {@link Manifest.permission#POST_NOTIFICATIONS}, apps can
+     * send users to its notification settings to enable notifications. See
+     * {@link android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS} for more information
+     * on launching notification settings.
+     * </p>
      *
      * @param activity The target activity.
      * @param permissions The requested permissions. Must be non-null and not empty.
@@ -497,6 +512,7 @@
      * @see #checkSelfPermission(android.content.Context, String)
      * @see #shouldShowRequestPermissionRationale(android.app.Activity, String)
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     public static void requestPermissions(final @NonNull Activity activity,
             final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) {
         if (sDelegate != null
@@ -505,11 +521,32 @@
             return;
         }
 
-        for (String permission : permissions) {
-            if (TextUtils.isEmpty(permission)) {
+        Set<Integer> indicesOfPermissionsToRemove = new HashSet<>();
+        for (int i = 0; i < permissions.length; i++) {
+            if (TextUtils.isEmpty(permissions[i])) {
                 throw new IllegalArgumentException("Permission request for permissions "
                         + Arrays.toString(permissions) + " must not contain null or empty values");
             }
+
+            if (!BuildCompat.isAtLeastT()) {
+                if (TextUtils.equals(permissions[i], Manifest.permission.POST_NOTIFICATIONS)) {
+                    indicesOfPermissionsToRemove.add(i);
+                }
+            }
+        }
+
+        int numPermissionsToRemove = indicesOfPermissionsToRemove.size();
+        final String[] permissionsArray = numPermissionsToRemove > 0
+                ? new String[permissions.length - numPermissionsToRemove] : permissions;
+        if (numPermissionsToRemove > 0) {
+            if (numPermissionsToRemove == permissions.length) {
+                return;
+            }
+            for (int i = 0, modifiedIndex = 0; i < permissions.length; i++) {
+                if (!indicesOfPermissionsToRemove.contains(i)) {
+                    permissionsArray[modifiedIndex++] = permissions[i];
+                }
+            }
         }
 
         if (Build.VERSION.SDK_INT >= 23) {
@@ -523,19 +560,19 @@
             handler.post(new Runnable() {
                 @Override
                 public void run() {
-                    final int[] grantResults = new int[permissions.length];
+                    final int[] grantResults = new int[permissionsArray.length];
 
                     PackageManager packageManager = activity.getPackageManager();
                     String packageName = activity.getPackageName();
 
-                    final int permissionCount = permissions.length;
+                    final int permissionCount = permissionsArray.length;
                     for (int i = 0; i < permissionCount; i++) {
                         grantResults[i] = packageManager.checkPermission(
-                                permissions[i], packageName);
+                                permissionsArray[i], packageName);
                     }
 
                     ((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
-                            requestCode, permissions, grantResults);
+                            requestCode, permissionsArray, grantResults);
                 }
             });
         }
@@ -551,8 +588,14 @@
      * @see #checkSelfPermission(Context, String)
      * @see #requestPermissions(Activity, String[], int)
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
             @NonNull String permission) {
+        if (!BuildCompat.isAtLeastT()
+                && TextUtils.equals(Manifest.permission.POST_NOTIFICATIONS, permission)) {
+            // notification permission doesn't exist before T
+            return false;
+        }
         if (Build.VERSION.SDK_INT >= 23) {
             return Api23Impl.shouldShowRequestPermissionRationale(activity, permission);
         }
diff --git a/core/core/src/main/java/androidx/core/app/ComponentActivity.java b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
index 764e041..206cfd2 100644
--- a/core/core/src/main/java/androidx/core/app/ComponentActivity.java
+++ b/core/core/src/main/java/androidx/core/app/ComponentActivity.java
@@ -28,8 +28,10 @@
 import androidx.annotation.CallSuper;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.collection.SimpleArrayMap;
+import androidx.core.os.BuildCompat;
 import androidx.core.view.KeyEventDispatcher;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
@@ -167,6 +169,7 @@
         return !shouldSkipDump(args);
     }
 
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     private static boolean shouldSkipDump(@Nullable String[] args) {
         if (args != null && args.length > 0) {
             // NOTE: values below are hardcoded on framework's Activity (like dumpInner())
@@ -177,6 +180,9 @@
                     return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
                 case "--translation":
                     return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
+                case "--list-dumpables":
+                case "--dump-dumpable":
+                    return BuildCompat.isAtLeastT();
             }
         }
         return false;
diff --git a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
index de76c7f..08343a4 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -794,6 +794,7 @@
          * Check the current list of enabled listener packages and update the records map
          * accordingly.
          */
+        @SuppressWarnings("deprecation")
         private void updateListenerMap() {
             Set<String> enabledPackages = getEnabledListenerPackages(mContext);
             if (enabledPackages.equals(mCachedEnabledPackages)) {
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index c209d4f..5cfe607 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -196,7 +196,7 @@
      * @param intent Intent that was launched to share content
      * @return ComponentName of the calling activity
      */
-    @SuppressWarnings("WeakerAccess")
+    @SuppressWarnings({"WeakerAccess", "deprecation"})
     @Nullable
     static ComponentName getCallingActivity(@NonNull Intent intent) {
         ComponentName result = intent.getParcelableExtra(EXTRA_CALLING_ACTIVITY);
@@ -882,6 +882,7 @@
          * @return A URI referring to a data stream to be shared or null if one was not supplied
          * @see Intent#EXTRA_STREAM
          */
+        @SuppressWarnings("deprecation")
         @Nullable
         public Uri getStream() {
             return mIntent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -896,6 +897,7 @@
          * @see Intent#EXTRA_STREAM
          * @see Intent#ACTION_SEND_MULTIPLE
          */
+        @SuppressWarnings("deprecation")
         @Nullable
         public Uri getStream(int index) {
             if (mStreams == null && isMultipleShare()) {
@@ -918,6 +920,7 @@
          *
          * @return Count of text items contained within the Intent
          */
+        @SuppressWarnings("deprecation")
         public int getStreamCount() {
             if (mStreams == null && isMultipleShare()) {
                 mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
@@ -1066,6 +1069,7 @@
          *
          * @return The calling application's label or null if unknown
          */
+        @SuppressWarnings("deprecation")
         @Nullable
         public CharSequence getCallingApplicationLabel() {
             if (mCallingPackage == null) return null;
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 942112e..2ac62dc 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -84,10 +84,12 @@
 import android.app.usage.UsageStatsManager;
 import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
 import android.content.ClipboardManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.RestrictionsManager;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -126,6 +128,7 @@
 import android.telecom.TelecomManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
@@ -139,16 +142,23 @@
 import androidx.annotation.ColorRes;
 import androidx.annotation.DoNotInline;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
 import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.app.NotificationManagerCompat;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BuildCompat;
 import androidx.core.os.EnvironmentCompat;
 import androidx.core.os.ExecutorCompat;
 import androidx.core.util.ObjectsCompat;
 
 import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.HashMap;
 import java.util.concurrent.Executor;
 
@@ -190,6 +200,35 @@
         return null;
     }
 
+
+    private static final String DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX =
+            ".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
+
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef(flag = true, value = {
+            RECEIVER_VISIBLE_TO_INSTANT_APPS, RECEIVER_EXPORTED, RECEIVER_NOT_EXPORTED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RegisterReceiverFlags {}
+    /**
+     * Flag for {@link #registerReceiver}: The receiver can receive broadcasts from Instant Apps.
+     */
+    public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 0x1;
+
+    /**
+     * Flag for {@link #registerReceiver}: The receiver can receive broadcasts from other Apps.
+     * Has the same behavior as marking a statically registered receiver with "exported=true"
+     */
+    public static final int RECEIVER_EXPORTED = 0x2;
+
+    /**
+     * Flag for {@link #registerReceiver}: The receiver cannot receive broadcasts from other Apps.
+     * Has the same behavior as marking a statically registered receiver with "exported=false"
+     */
+    public static final int RECEIVER_NOT_EXPORTED = 0x4;
+
     /**
      * Start a set of activities as a synthesized task stack, if able.
      *
@@ -550,8 +589,15 @@
      * permission, or {@link PackageManager#PERMISSION_DENIED} if not.
      * @see PackageManager#checkPermission(String, String)
      */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
     public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) {
         ObjectsCompat.requireNonNull(permission, "permission must be non-null");
+        if (!BuildCompat.isAtLeastT()
+                && TextUtils.equals(android.Manifest.permission.POST_NOTIFICATIONS, permission)) {
+            return NotificationManagerCompat.from(context).areNotificationsEnabled()
+                    ? PackageManager.PERMISSION_GRANTED
+                    : PackageManager.PERMISSION_DENIED;
+        }
         return context.checkPermission(permission, Process.myPid(), Process.myUid());
     }
 
@@ -727,6 +773,80 @@
     }
 
     /**
+     * Register a broadcast receiver.
+     *
+     * @param context  Context to retrieve service from.
+     * @param receiver The BroadcastReceiver to handle the broadcast.
+     * @param filter   Selects the Intent broadcasts to be received.
+     * @param flags    Specify one of {@link #RECEIVER_EXPORTED}, if you wish for your receiver
+     *                 to be able to receiver broadcasts from other applications, or
+     *                 {@link #RECEIVER_NOT_EXPORTED} if you only want your receiver to be able
+     *                 to receive broadcasts from the system or your own app.
+     * @return The first sticky intent found that matches <var>filter</var>,
+     * or null if there are none.
+     * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, int)
+     */
+    @Nullable
+    public static Intent registerReceiver(@NonNull Context context,
+            @Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
+            @RegisterReceiverFlags int flags) {
+        return registerReceiver(context, receiver, filter, null, null, flags);
+    }
+
+    /**
+     * Register a broadcast receiver.
+     *
+     * @param context             Context to retrieve service from.
+     * @param receiver            The BroadcastReceiver to handle the broadcast.
+     * @param filter              Selects the Intent broadcasts to be received.
+     * @param broadcastPermission String naming a permission that a broadcaster must hold in
+     *                            order to send and Intent to you. If null, no permission is
+     *                            required.
+     * @param scheduler           Handler identifying the thread will receive the Intent. If
+     *                            null, the main thread of the process will be used.
+     * @param flags               Specify one of {@link #RECEIVER_EXPORTED}, if you wish for your
+     *                            receiver to be able to receiver broadcasts from other
+     *                            applications, or {@link #RECEIVER_NOT_EXPORTED} if you only want
+     *                            your receiver to be able to receive broadcasts from the system
+     *                            or your own app.
+     * @return The first sticky intent found that matches <var>filter</var>,
+     * or null if there are none.
+     * @see Context#registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int)
+     */
+    @Nullable
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public static Intent registerReceiver(@NonNull Context context,
+            @Nullable BroadcastReceiver receiver, @NonNull IntentFilter filter,
+            @Nullable String broadcastPermission,
+            @Nullable Handler scheduler, @RegisterReceiverFlags int flags) {
+        if (((flags & RECEIVER_EXPORTED) == 0) && ((flags & RECEIVER_NOT_EXPORTED) == 0)) {
+            throw new IllegalArgumentException("One of either RECEIVER_EXPORTED or "
+                    + "RECEIVER_NOT_EXPORTED is required");
+        }
+
+        if (((flags & RECEIVER_EXPORTED) != 0) && ((flags & RECEIVER_NOT_EXPORTED) != 0)) {
+            throw new IllegalArgumentException("Cannot specify both RECEIVER_EXPORTED and "
+                    + "RECEIVER_NOT_EXPORTED");
+        }
+
+        if (BuildCompat.isAtLeastT()) {
+            return Api33Impl.registerReceiver(context, receiver, filter, broadcastPermission,
+                    scheduler, flags);
+        }
+        if (Build.VERSION.SDK_INT >= 26) {
+            return Api26Impl.registerReceiver(context, receiver, filter, broadcastPermission,
+                    scheduler, flags);
+        }
+        if (((flags & RECEIVER_NOT_EXPORTED) != 0) && (broadcastPermission == null)) {
+            String permission =
+                    context.getPackageName() + DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX;
+            return context.registerReceiver(receiver, filter, permission, scheduler /* handler */);
+        }
+        return context.registerReceiver(receiver, filter, broadcastPermission,
+                scheduler);
+    }
+
+    /**
      * Gets the name of the system-level service that is represented by the specified class.
      *
      * @param context      Context to retrieve service name from.
@@ -925,6 +1045,19 @@
             // This class is not instantiable.
         }
 
+        @DoNotInline
+        static Intent registerReceiver(Context obj, @Nullable BroadcastReceiver receiver,
+                IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) {
+            if ((flags & RECEIVER_NOT_EXPORTED) != 0 && broadcastPermission == null) {
+                String permission =
+                        obj.getPackageName() + DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION_SUFFIX;
+                // receivers that are not exported should also not be visible to instant apps
+                return obj.registerReceiver(receiver, filter, permission, scheduler);
+            }
+            flags &= Context.RECEIVER_VISIBLE_TO_INSTANT_APPS;
+            return obj.registerReceiver(receiver, filter, broadcastPermission, scheduler, flags);
+        }
+
         @SuppressWarnings("UnusedReturnValue")
         @DoNotInline
         static ComponentName startForegroundService(Context obj, Intent service) {
@@ -955,4 +1088,17 @@
             return obj.getAttributionTag();
         }
     }
+
+    @RequiresApi(33)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable
+        }
+
+        @DoNotInline
+        static Intent registerReceiver(Context obj, @Nullable BroadcastReceiver receiver,
+                IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) {
+            return obj.registerReceiver(receiver, filter, broadcastPermission, scheduler, flags);
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
index 8831564..f932e1d 100644
--- a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
@@ -217,6 +217,7 @@
      */
     @Nullable
     @RestrictTo(LIBRARY)
+    @SuppressWarnings("deprecation")
     public static String getPermissionRevocationVerifierApp(
             @NonNull PackageManager packageManager) {
         Intent permissionRevocationSettingsIntent =
diff --git a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
index 9a037c7..31372ce 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
@@ -81,6 +81,7 @@
      *                                              provided {@param packageManager}
      */
     @NonNull
+    @SuppressWarnings("deprecation")
     public static List<Signature> getSignatures(@NonNull PackageManager packageManager,
             @NonNull String packageName) throws PackageManager.NameNotFoundException {
         Signature[] array;
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
index 33b02cb..169aba6 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutManagerCompat.java
@@ -860,6 +860,7 @@
         return sShortcutInfoCompatSaver;
     }
 
+    @SuppressWarnings("deprecation")
     private static List<ShortcutInfoChangeListener> getShortcutInfoListeners(Context context) {
         if (sShortcutInfoChangeListeners == null) {
             List<ShortcutInfoChangeListener> result = new ArrayList<>();
diff --git a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
index 95a9003..5b1e2ef 100644
--- a/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
+++ b/core/core/src/main/java/androidx/core/content/pm/ShortcutXmlParser.java
@@ -86,6 +86,7 @@
      * Returns a set of string which contains the ids of static shortcuts.
      */
     @NonNull
+    @SuppressWarnings("deprecation")
     private static Set<String> parseShortcutIds(@NonNull final Context context) {
         final Set<String> result = new HashSet<>();
         final Intent mainIntent = new Intent(Intent.ACTION_MAIN);
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index dd4b95a..8a8385c 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -682,6 +682,7 @@
         return null;
     }
 
+    @SuppressWarnings("deprecation")
     static Resources getResources(Context context, String resPackage) {
         if ("android".equals(resPackage)) {
             return Resources.getSystem();
diff --git a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
index 3c2c8f1..47c9dca 100644
--- a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
@@ -137,6 +137,7 @@
      * potentially-stale value from
      * {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}.
      */
+    @SuppressWarnings("deprecation")
     @SuppressLint("ReferencesDeprecated")
     @Nullable
     @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index 988a6d5..244040a 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -200,10 +200,10 @@
      * removed and all calls must be replaced with {@code Build.VERSION.SDK_INT >=
      * Build.VERSION_CODES.TIRAMISU}.
      *
-     * @return {@code true} if T APIs are available for use, {@code false} otherwise
+     * @return {@code true} if Tiramisu APIs are available for use, {@code false} otherwise
      */
     @PrereleaseSdkCheck
-    @ChecksSdkIntAtLeast(codename = "Tiramisu")
+    @ChecksSdkIntAtLeast(api = 33) // 33 is wrong but required for NewApi, see b/204776549
     public static boolean isAtLeastT() {
         return VERSION.SDK_INT >= 32 && isAtLeastPreReleaseCodename("Tiramisu", VERSION.CODENAME);
     }
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
index a09c8bf..7409305 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -23,8 +23,10 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.Size;
+import androidx.core.text.ICUCompat;
 
 import java.util.Locale;
 
@@ -231,6 +233,87 @@
         }
     }
 
+    /**
+     * Determine whether two locales are considered a match, even if they are not exactly equal.
+     * They are considered as a match when both of their languages and scripts
+     * (explicit or inferred) are identical. This means that a user would be able to understand
+     * the content written in the supported locale even if they say they prefer the desired locale.
+     *
+     * E.g. [zh-HK] matches [zh-Hant]; [en-US] matches [en-CA].
+     *
+     * @param supported The supported {@link Locale} to be compared.
+     * @param desired   The desired {@link Locale} to be compared.
+     * @return True if they match, false otherwise.
+     */
+    @RequiresApi(21)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public static boolean matchesLanguageAndScript(@NonNull Locale supported,
+            @NonNull Locale desired) {
+        if (BuildCompat.isAtLeastT()) {
+            return LocaleList.matchesLanguageAndScript(supported, desired);
+        } else if (Build.VERSION.SDK_INT >= 21) {
+            return Api21Impl.matchesLanguageAndScript(supported, desired);
+        } else {
+            throw new UnsupportedOperationException(
+                    "This method is only supported on API level 21+");
+        }
+    }
+
+    @RequiresApi(21)
+    static class Api21Impl {
+        private Api21Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static boolean matchesLanguageAndScript(@NonNull Locale supported,
+                @NonNull Locale desired) {
+            if (supported.equals(desired)) {
+                return true;  // return early so we don't do unnecessary computation
+            }
+            if (!supported.getLanguage().equals(desired.getLanguage())) {
+                return false;
+            }
+            if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
+                // The locales are not the same, but the languages are the same, and one of the
+                // locales
+                // is a pseudo-locale. So this is not a match.
+                return false;
+            }
+            final String supportedScr = ICUCompat.maximizeAndGetScript(supported);
+            if (supportedScr.isEmpty()) {
+                // If we can't guess a script, we don't know enough about the locales' language
+                // to find
+                // if the locales match. So we fall back to old behavior of matching, which
+                // considered
+                // locales with different regions different.
+                final String supportedRegion = supported.getCountry();
+                return supportedRegion.isEmpty() || supportedRegion.equals(desired.getCountry());
+            }
+            final String desiredScr = ICUCompat.maximizeAndGetScript(desired);
+            // There is no match if the two locales use different scripts. This will most imporantly
+            // take care of traditional vs simplified Chinese.
+            return supportedScr.equals(desiredScr);
+        }
+
+        private static final Locale[] PSEUDO_LOCALE = {
+                new Locale("en", "XA"), new Locale("ar", "XB")};
+
+        private static boolean isPseudoLocale(Locale locale) {
+            for (Locale pseudoLocale : PSEUDO_LOCALE) {
+                if (pseudoLocale.equals(locale)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @DoNotInline
+        static Locale forLanguageTag(String languageTag) {
+            return Locale.forLanguageTag(languageTag);
+        }
+    }
+
     @Override
     public boolean equals(Object other) {
         return other instanceof LocaleListCompat && mImpl.equals(((LocaleListCompat) other).mImpl);
@@ -268,16 +351,4 @@
             return LocaleList.getDefault();
         }
     }
-
-    @RequiresApi(21)
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        @DoNotInline
-        static Locale forLanguageTag(String languageTag) {
-            return Locale.forLanguageTag(languageTag);
-        }
-    }
 }
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index a181077..8fa56eb 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -16,9 +16,23 @@
 
 package androidx.core.os;
 
+import android.annotation.SuppressLint;
+import android.os.Build;
 import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Helper for accessing features in {@link Parcel}.
@@ -43,5 +57,329 @@
         out.writeInt(value ? 1 : 0);
     }
 
+    /**
+     * Same as {@link Parcel#readList(List, ClassLoader)} but accepts {@code clazz} parameter as
+     * the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings("deprecation")
+    public static <T> void readList(@NonNull Parcel in, @NonNull List<? super T> outVal,
+            @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            TiramisuImpl.readList(in, outVal, loader, clazz);
+        } else {
+            in.readList(outVal, loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readArrayList(ClassLoader)} but accepts {@code clazz} parameter as
+     * the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressLint({"ConcreteCollection", "NullableCollection"})
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @Nullable
+    public static <T> ArrayList<T> readArrayList(@NonNull Parcel in, @Nullable ClassLoader loader,
+            @NonNull Class<? extends T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readArrayList(in, loader, clazz);
+        } else {
+            return in.readArrayList(loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readArray(ClassLoader)} but accepts {@code clazz} parameter as
+     * the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @SuppressLint({"ArrayReturn", "NullableCollection"})
+    @Nullable
+    public static <T> T[] readArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+            @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readArray(in, loader, clazz);
+        } else {
+            return (T[]) in.readArray(loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readSparseArray(ClassLoader)} but accepts {@code clazz} parameter as
+     * the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings("deprecation")
+    @Nullable
+    public static <T> SparseArray<T> readSparseArray(@NonNull Parcel in,
+            @Nullable ClassLoader loader,
+            @NonNull Class<? extends T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readSparseArray(in, loader, clazz);
+        } else {
+            return in.readSparseArray(loader);
+        }
+    }
+
+
+    /**
+     * Same as {@link Parcel#readMap(Map, ClassLoader)} but accepts {@code clazzKey} and
+     * {@code clazzValue} parameter as the types required for each key and value pair.
+     *
+     * @throws android.os.BadParcelableException If the item to be deserialized is not an
+     * instance of that class or any of its children class.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings("deprecation")
+    public static <K, V> void readMap(@NonNull Parcel in, @NonNull Map<? super K, ? super V> outVal,
+            @Nullable ClassLoader loader, @NonNull Class<K> clazzKey,
+            @NonNull Class<V> clazzValue) {
+        if (BuildCompat.isAtLeastT()) {
+            TiramisuImpl.readMap(in, outVal, loader, clazzKey, clazzValue);
+        } else {
+            in.readMap(outVal, loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readHashMap(ClassLoader)} but accepts {@code clazzKey} and
+     * {@code clazzValue} parameter as the types required for each key and value pair.
+     *
+     * @throws android.os.BadParcelableException if the item to be deserialized is not an
+     * instance of that class or any of its children class.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressLint({"ConcreteCollection", "NullableCollection"})
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @Nullable
+    public static <K, V> HashMap<K, V> readHashMap(@NonNull Parcel in, @Nullable ClassLoader loader,
+            @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readHashMap(in, loader, clazzKey, clazzValue);
+        } else {
+            return in.readHashMap(loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readParcelable(ClassLoader)} but accepts {@code clazz} parameter as
+     * the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings("deprecation")
+    @Nullable
+    public static <T extends Parcelable> T readParcelable(@NonNull Parcel in,
+            @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readParcelable(in, loader, clazz);
+        } else {
+            return in.readParcelable(loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readParcelableCreator(ClassLoader)} but accepts {@code clazz} parameter
+     * as the required type.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there
+     * there was an error trying to read the {@link Parcelable.Creator}.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @Nullable
+    @RequiresApi(30)
+    public static <T> Parcelable.Creator<T> readParcelableCreator(@NonNull Parcel in,
+            @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readParcelableCreator(in, loader, clazz);
+        } else {
+            return (Parcelable.Creator<T>) Api30Impl.readParcelableCreator(in, loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readParcelableArray(ClassLoader)}  but accepts {@code clazz} parameter
+     * as the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @SuppressLint({"ArrayReturn", "NullableCollection"})
+    @Nullable
+    public static <T> T[] readParcelableArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+            @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readParcelableArray(in, loader, clazz);
+        } else {
+            return (T[]) in.readParcelableArray(loader);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readParcelableList(List, ClassLoader)} but accepts {@code clazz}
+     * parameter as the type required for each item.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children classes or there was
+     * an error trying to instantiate an element.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @NonNull
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @RequiresApi(api = Build.VERSION_CODES.Q)
+    public static <T> List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
+            @Nullable ClassLoader cl, @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readParcelableList(in, list, cl, clazz);
+        } else {
+            return Api29Impl.readParcelableList(in, (List) list, cl);
+        }
+    }
+
+    /**
+     * Same as {@link Parcel#readSerializable()} but accepts {@code loader} parameter
+     * as the primary classLoader for resolving the Serializable class; and {@code clazz} parameter
+     * as the required type.
+     *
+     * @throws android.os.BadParcelableException Throws BadParcelableException if the item to be
+     * deserialized is not an instance of that class or any of its children class or there there
+     * was an error deserializing the object.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings({"deprecation", "unchecked"})
+    @Nullable
+    public static <T extends Serializable> T readSerializable(@NonNull Parcel in,
+            @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+        if (BuildCompat.isAtLeastT()) {
+            return TiramisuImpl.readSerializable(in, loader, clazz);
+        } else {
+            return (T) in.readSerializable();
+        }
+    }
+
     private ParcelCompat() {}
+
+    @RequiresApi(29)
+    static class Api29Impl {
+        private Api29Impl() {
+            // This class is non-instantiable.
+        }
+
+        @DoNotInline
+        static final <T extends Parcelable> List<T> readParcelableList(@NonNull Parcel in,
+                @NonNull List<T> list, @Nullable ClassLoader cl) {
+            return in.readParcelableList(list, cl);
+        }
+    }
+
+    @RequiresApi(30)
+    static class Api30Impl {
+        private Api30Impl() {
+            // This class is non-instantiable.
+        }
+
+        @DoNotInline
+        static final Parcelable.Creator<?> readParcelableCreator(@NonNull Parcel in,
+                @Nullable ClassLoader loader) {
+            return in.readParcelableCreator(loader);
+        }
+    }
+
+    @RequiresApi(33)
+    static class TiramisuImpl {
+        private TiramisuImpl() {
+            // This class is non-instantiable.
+        }
+
+        @DoNotInline
+        static <T extends Serializable> T readSerializable(@NonNull Parcel in,
+                @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+            return in.readSerializable(loader, clazz);
+        }
+
+        @DoNotInline
+        static <T extends Parcelable> T readParcelable(@NonNull Parcel in,
+                @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+            return in.readParcelable(loader, clazz);
+        }
+
+        @DoNotInline
+        public static <T> Parcelable.Creator<T> readParcelableCreator(Parcel in, ClassLoader loader,
+                Class<T> clazz) {
+            return in.readParcelableCreator(loader, clazz);
+        }
+
+        @DoNotInline
+        static <T> T[] readParcelableArray(@NonNull Parcel in, @Nullable ClassLoader loader,
+                @NonNull Class<T> clazz) {
+            return in.readParcelableArray(loader, clazz);
+        }
+
+        @DoNotInline
+        static <T> List<T> readParcelableList(@NonNull Parcel in, @NonNull List<T> list,
+                @Nullable ClassLoader cl, @NonNull Class<T> clazz) {
+            return in.readParcelableList(list, cl, clazz);
+        }
+
+        @DoNotInline
+        public static <T> void readList(@NonNull Parcel in, @NonNull List<? super T> outVal,
+                @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+            in.readList(outVal, loader, clazz);
+        }
+
+        @DoNotInline
+        public static <T> ArrayList<T> readArrayList(Parcel in, ClassLoader loader,
+                Class<? extends T> clazz) {
+            return in.readArrayList(loader, clazz);
+        }
+
+        @DoNotInline
+        public static <T> T[] readArray(Parcel in, ClassLoader loader, Class<T> clazz) {
+            return in.readArray(loader, clazz);
+        }
+
+        @DoNotInline
+        public static <T> SparseArray<T> readSparseArray(Parcel in, ClassLoader loader,
+                Class<? extends T> clazz) {
+            return in.readSparseArray(loader, clazz);
+        }
+
+        @DoNotInline
+        public static <K, V> void readMap(Parcel in, Map<? super K, ? super V> outVal,
+                ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {
+            in.readMap(outVal, loader, clazzKey, clazzValue);
+        }
+
+        @DoNotInline
+        public static <V, K> HashMap<K, V> readHashMap(Parcel in, ClassLoader loader,
+                Class<? extends K> clazzKey, Class<? extends V> clazzValue) {
+            return in.readHashMap(loader, clazzKey, clazzValue);
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
index ed6beeb..74a0ae7 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityEventCompat.java
@@ -202,6 +202,36 @@
     public static final int CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 0x00000040;
 
     /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag has started while accessibility is enabled. This is either via an
+     * AccessibilityAction, or via touch events. This is sent from the source that initiated the
+     * drag.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_START
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 0x00000080;
+
+    /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag in with accessibility enabled has ended. This means the content has been
+     * successfully dropped. This is sent from the target that accepted the dragged content.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_DROP
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_DROPPED = 0x00000100;
+
+    /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+     * A drag in with accessibility enabled has ended. This means the content has been
+     * unsuccessfully dropped, the user has canceled the action via an AccessibilityAction, or
+     * no drop has been detected within a timeout and the drag was automatically cancelled. This is
+     * sent from the source that initiated the drag.
+     *
+     * @see AccessibilityNodeInfo.AccessibilityAction#ACTION_DRAG_CANCEL
+     */
+    public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 0x0000200;
+
+    /**
      * Mask for {@link AccessibilityEvent} all types.
      *
      * @see AccessibilityEvent#TYPE_VIEW_CLICKED
@@ -238,7 +268,10 @@
                     CONTENT_CHANGE_TYPE_STATE_DESCRIPTION,
                     CONTENT_CHANGE_TYPE_SUBTREE,
                     CONTENT_CHANGE_TYPE_TEXT,
-                    CONTENT_CHANGE_TYPE_UNDEFINED
+                    CONTENT_CHANGE_TYPE_UNDEFINED,
+                    CONTENT_CHANGE_TYPE_DRAG_STARTED,
+                    CONTENT_CHANGE_TYPE_DRAG_DROPPED,
+                    CONTENT_CHANGE_TYPE_DRAG_CANCELLED
             })
     @RestrictTo(LIBRARY_GROUP_PREFIX)
     @Retention(RetentionPolicy.SOURCE)
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 446b6dc..2a05851 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -23,6 +23,7 @@
 import static java.util.Collections.emptyList;
 
 import android.annotation.SuppressLint;
+import android.content.ClipData;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.Build;
@@ -42,6 +43,7 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.core.R;
 import androidx.core.accessibilityservice.AccessibilityServiceInfoCompat;
@@ -591,6 +593,54 @@
                         ? AccessibilityNodeInfo.AccessibilityAction.ACTION_IME_ENTER : null,
                         android.R.id.accessibilityActionImeEnter, null, null, null);
 
+        /**
+         * Action to start a drag.
+         * <p>
+         * This action initiates a drag & drop within the system. The source's dragged content is
+         * prepared before the drag begins. In View, this action should prepare the arguments to
+         * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}} and then
+         * call the method. The equivalent should be performed for other UI toolkits.
+         * </p>
+         *
+         * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_STARTED
+         */
+        @NonNull
+        public static final AccessibilityActionCompat ACTION_DRAG_START =
+                new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+                        ?  AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START : null,
+                        android.R.id.accessibilityActionDragStart, null, null, null);
+
+        /**
+         * Action to trigger a drop of the content being dragged.
+         * <p>
+         * This action is added to potential drop targets if the source started a drag with
+         * {@link #ACTION_DRAG_START}. In View, these targets are Views that accepted
+         * {@link android.view.DragEvent#ACTION_DRAG_STARTED} and have an
+         * {@link View.OnDragListener}.
+         * </p>
+         *
+         * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_DROPPED
+         */
+        @NonNull
+        public static final AccessibilityActionCompat ACTION_DRAG_DROP =
+                new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+                        ?  AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP : null,
+                        android.R.id.accessibilityActionDragDrop, null, null, null);
+
+        /**
+         * Action to cancel a drag.
+         * <p>
+         * This action is added to the source that started a drag with {@link #ACTION_DRAG_START}.
+         * </p>
+         *
+         * @see AccessibilityEventCompat#CONTENT_CHANGE_TYPE_DRAG_CANCELLED
+         */
+        @NonNull
+        public static final AccessibilityActionCompat ACTION_DRAG_CANCEL =
+                new AccessibilityActionCompat(Build.VERSION.SDK_INT >= 32
+                        ?  AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL : null,
+                        android.R.id.accessibilityActionDragCancel, null, null, null);
+
         final Object mAction;
         private final int mId;
         private final Class<? extends CommandArguments> mViewCommandArgumentClass;
@@ -1235,6 +1285,9 @@
     private static final String STATE_DESCRIPTION_KEY =
             "androidx.view.accessibility.AccessibilityNodeInfoCompat.STATE_DESCRIPTION_KEY";
 
+    private static final String UNIQUE_ID_KEY =
+            "androidx.view.accessibility.AccessibilityNodeInfoCompat.UNIQUE_ID_KEY";
+
     // These don't line up with the internal framework constants, since they are independent
     // and we might as well get all 32 bits of utility here.
     private static final int BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE = 0x00000001;
@@ -2854,6 +2907,42 @@
     }
 
     /**
+     * Gets the unique id of this node.
+     *
+     * @return the unique id or null if android version smaller
+     * than 19.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public @Nullable String getUniqueId() {
+        if (BuildCompat.isAtLeastT()) {
+            return mInfo.getUniqueId();
+        } else if (Build.VERSION.SDK_INT >= 19) {
+            return mInfo.getExtras().getString(UNIQUE_ID_KEY);
+        }
+        return null;
+    }
+
+    /**
+     * Sets the unique id of this node.
+     * <p>
+     *   <strong>Note:</strong> Cannot be called from an
+     *   {@link android.accessibilityservice.AccessibilityService}.
+     *   This class is made immutable before being delivered to an AccessibilityService.
+     * </p>
+     *
+     * @param uniqueId the unique id of this node.
+     * @throws IllegalStateException If called from an AccessibilityService.
+     */
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public void setUniqueId(@Nullable String uniqueId) {
+        if (BuildCompat.isAtLeastT()) {
+            mInfo.setUniqueId(uniqueId);
+        } else if (Build.VERSION.SDK_INT >= 19) {
+            mInfo.getExtras().putString(UNIQUE_ID_KEY, uniqueId);
+        }
+    }
+
+    /**
      * Return an instance back to be reused.
      * <p>
      * <strong>Note:</strong> You must not touch the object after calling this function.
@@ -4031,7 +4120,7 @@
      */
     public @Nullable CharSequence getRoleDescription() {
         if (Build.VERSION.SDK_INT >= 19) {
-            return mInfo.getExtras().getCharSequence(ROLE_DESCRIPTION_KEY);
+            return getExtras().getCharSequence(ROLE_DESCRIPTION_KEY);
         } else {
             return null;
         }
@@ -4063,7 +4152,7 @@
      */
     public void setRoleDescription(@Nullable CharSequence roleDescription) {
         if (Build.VERSION.SDK_INT >= 19) {
-            mInfo.getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, roleDescription);
+            getExtras().putCharSequence(ROLE_DESCRIPTION_KEY, roleDescription);
         }
     }
 
@@ -4169,6 +4258,7 @@
         builder.append("; text: ").append(getText());
         builder.append("; contentDescription: ").append(getContentDescription());
         builder.append("; viewId: ").append(getViewIdResourceName());
+        builder.append("; uniqueId: ").append(getUniqueId());
 
         builder.append("; checkable: ").append(isCheckable());
         builder.append("; checked: ").append(isChecked());
@@ -4304,8 +4394,14 @@
                 return "ACTION_PRESS_AND_HOLD";
             case android.R.id.accessibilityActionImeEnter:
                 return "ACTION_IME_ENTER";
+            case android.R.id.accessibilityActionDragStart:
+                return "ACTION_DRAG_START";
+            case android.R.id.accessibilityActionDragDrop:
+                return "ACTION_DRAG_DROP";
+            case android.R.id.accessibilityActionDragCancel:
+                return "ACTION_DRAG_CANCEL";
             default:
-                return"ACTION_UNKNOWN";
+                return "ACTION_UNKNOWN";
         }
     }
 }
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 7a6a655..dfb25e3 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -650,6 +650,7 @@
             }
         }
 
+        @SuppressWarnings("deprecation")
         private List<ResolveInfo> getSupportedActivities(final Context context,
                 final PackageManager packageManager) {
             final List<ResolveInfo> supportedActivities = new ArrayList<>();
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-public/build.gradle b/docs-public/build.gradle
index bb9f1b7..400ebac 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,16 +4,16 @@
 }
 
 dependencies {
-    docs("androidx.activity:activity:1.5.0-alpha05")
-    docs("androidx.activity:activity-compose:1.5.0-alpha05")
-    samples("androidx.activity:activity-compose-samples:1.5.0-alpha05")
-    docs("androidx.activity:activity-ktx:1.5.0-alpha05")
+    docs("androidx.activity:activity:1.6.0-alpha01")
+    docs("androidx.activity:activity-compose:1.6.0-alpha01")
+    samples("androidx.activity:activity-compose-samples:1.6.0-alpha01")
+    docs("androidx.activity:activity-ktx:1.6.0-alpha01")
     docs("androidx.ads:ads-identifier:1.0.0-alpha04")
     docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
     docs("androidx.annotation:annotation:1.4.0-alpha02")
     docs("androidx.annotation:annotation-experimental:1.2.0")
-    docs("androidx.appcompat:appcompat:1.5.0-alpha01")
-    docs("androidx.appcompat:appcompat-resources:1.5.0-alpha01")
+    docs("androidx.appcompat:appcompat:1.6.0-alpha01")
+    docs("androidx.appcompat:appcompat-resources:1.6.0-alpha01")
     docs("androidx.appsearch:appsearch:1.0.0-alpha04")
     docs("androidx.appsearch:appsearch-ktx:1.0.0-alpha04")
     docs("androidx.appsearch:appsearch-local-storage:1.0.0-alpha04")
@@ -103,8 +103,8 @@
     docs("androidx.core:core-role:1.1.0-rc01")
     docs("androidx.core:core-animation:1.0.0-alpha02")
     docs("androidx.core:core-animation-testing:1.0.0-alpha02")
-    docs("androidx.core:core:1.8.0-alpha07")
-    docs("androidx.core:core-ktx:1.8.0-alpha07")
+    docs("androidx.core:core:1.9.0-alpha01")
+    docs("androidx.core:core-ktx:1.9.0-alpha02")
     docs("androidx.core:core-splashscreen:1.0.0-beta02")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.2.0-alpha01")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 925cdf1..a3d4ef5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -145,6 +145,7 @@
     docs(project(":glance:glance"))
     docs(project(":glance:glance-appwidget"))
     docs(project(":glance:glance-wear-tiles"))
+    docs(project(":graphics:graphics-core"))
     docs(project(":gridlayout:gridlayout"))
     docs(project(":health:health-data-client"))
     docs(project(":health:health-services-client"))
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index e5d2274..e1e449f 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -5968,7 +5968,11 @@
                 throw new UnsupportedOperationException("Failed to read EXIF from HEIF file. "
                         + "Given stream is either malformed or unsupported.");
             } finally {
-                retriever.release();
+                try {
+                    retriever.release();
+                } catch (IOException e) {
+                    // Nothing we can  do about it.
+                }
             }
         } else {
             throw new UnsupportedOperationException("Reading EXIF from HEIF files "
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 020f2e2..ddefee4 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -25,7 +25,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.core:core-ktx:1.2.0")
+    api(projectOrArtifact(":core:core"))
     api("androidx.collection:collection:1.1.0")
     api("androidx.viewpager:viewpager:1.0.0")
     api("androidx.loader:loader:1.0.0")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
index 486e1fa..0905d7e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
@@ -82,6 +82,7 @@
  *
  * @see applyTrampolineIntent
  */
+@Suppress("DEPRECATION")
 internal fun Activity.launchTrampolineAction(intent: Intent) {
     val actionIntent = requireNotNull(intent.getParcelableExtra<Intent>(ActionIntentKey)) {
         "List adapter activity trampoline invoked without specifying target intent."
diff --git a/gradle.properties b/gradle.properties
index e6166a5..30a77bb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,8 +21,11 @@
 # Workaround for b/141364941
 android.forceJacocoOutOfProcess=true
 
-# Generate versioned API files
-androidx.writeVersionedApiFiles=true
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
+
+# Don't warn about needing to update AGP
+android.suppressUnsupportedCompileSdk=Tiramisu
 
 # Disable features we do not use
 android.defaults.buildfeatures.aidl=false
diff --git a/graphics/OWNERS b/graphics/OWNERS
new file mode 100644
index 0000000..db046a2
--- /dev/null
+++ b/graphics/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1137062
+sumir@google.com
+jreck@google.com
+xxayedawgxx@google.com
+njawad@google.com
\ No newline at end of file
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
new file mode 100644
index 0000000..ec0fb93
--- /dev/null
+++ b/graphics/graphics-core/api/current.txt
@@ -0,0 +1,205 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+  public final class GLRenderer {
+    ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public boolean isRunning();
+    method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+    method public void start(optional String name);
+    method public void start();
+    method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+    method public void stop(boolean cancelPending);
+    method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+  }
+
+  public static final class GLRenderer.Companion {
+  }
+
+  public static interface GLRenderer.EglContextCallback {
+    method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+  }
+
+  public static interface GLRenderer.RenderCallback {
+    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+  }
+
+  public static final class GLRenderer.RenderTarget {
+    method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(boolean cancelPending);
+    method public boolean isAttached();
+    method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender();
+    method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(int width, int height);
+  }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+  public final inline class EglConfigAttributes {
+    ctor public EglConfigAttributes();
+  }
+
+  public static final class EglConfigAttributes.Builder {
+    method public void include(int[] attributes);
+    method public infix void to(int, int that);
+  }
+
+  public final class EglConfigAttributesKt {
+    method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+    method public static int[] getEglConfigAttributes1010102();
+    method public static int[] getEglConfigAttributes8888();
+    method public static int[] getEglConfigAttributesF16();
+    property public static final int[] EglConfigAttributes1010102;
+    property public static final int[] EglConfigAttributes8888;
+    property public static final int[] EglConfigAttributesF16;
+    field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+    field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+    field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+  }
+
+  public final class EglException extends java.lang.RuntimeException {
+    ctor public EglException(int error, optional String msg);
+    method public int getError();
+    method public String getMsg();
+    property public final int error;
+    property public String message;
+    property public final String msg;
+  }
+
+  public final inline class EglExtensions {
+    ctor public EglExtensions();
+  }
+
+  public static final class EglExtensions.Companion {
+    method public java.util.Set<? extends java.lang.String> from(String queryString);
+  }
+
+  public final class EglExtensionsKt {
+    field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+    field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+    field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+    field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+    field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+    field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+    field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+    field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+    field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+    field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+    field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+    field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+    field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+    field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+  }
+
+  public final class EglManager {
+    ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+    method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface getCurrentDrawSurface();
+    method public android.opengl.EGLSurface getCurrentReadSurface();
+    method public android.opengl.EGLSurface getDefaultSurface();
+    method public android.opengl.EGLConfig? getEglConfig();
+    method public android.opengl.EGLContext? getEglContext();
+    method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+    method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+    method public void initialize();
+    method public boolean isExtensionSupported(String extensionName);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+    method public void release();
+    method public void swapAndFlushBuffers();
+    property public final android.opengl.EGLSurface currentDrawSurface;
+    property public final android.opengl.EGLSurface currentReadSurface;
+    property public final android.opengl.EGLSurface defaultSurface;
+    property public final android.opengl.EGLConfig? eglConfig;
+    property public final android.opengl.EGLContext? eglContext;
+    property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+    property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+    field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+  }
+
+  public static final class EglManager.Companion {
+  }
+
+  public interface EglSpec {
+    method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+    method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+    method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+    method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+    method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+    method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+    method public int eglGetError();
+    method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+    method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+    method public String eglQueryString(int nameId);
+    method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+    method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+    method public default String getErrorMessage();
+    method public default static String getStatusString(int error);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+  }
+
+  public static final class EglSpec.Companion {
+    method public String getStatusString(int error);
+  }
+
+  public final class EglVersion {
+    ctor public EglVersion(int major, int minor);
+    method public int component1();
+    method public int component2();
+    method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+    method public int getMajor();
+    method public int getMinor();
+    property public final int major;
+    property public final int minor;
+    field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+    field public static final androidx.graphics.opengl.egl.EglVersion V14;
+    field public static final androidx.graphics.opengl.egl.EglVersion V15;
+  }
+
+  public static final class EglVersion.Companion {
+  }
+
+}
+
+package androidx.graphics.surface {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+    ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+    method protected void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Companion {
+  }
+
+  public static class SurfaceControlCompat.Transaction {
+    ctor public SurfaceControlCompat.Transaction();
+    method public final void delete();
+    method public final void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Transaction.Companion {
+  }
+
+}
+
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..ec0fb93
--- /dev/null
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -0,0 +1,205 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+  public final class GLRenderer {
+    ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public boolean isRunning();
+    method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+    method public void start(optional String name);
+    method public void start();
+    method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+    method public void stop(boolean cancelPending);
+    method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+  }
+
+  public static final class GLRenderer.Companion {
+  }
+
+  public static interface GLRenderer.EglContextCallback {
+    method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+  }
+
+  public static interface GLRenderer.RenderCallback {
+    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+  }
+
+  public static final class GLRenderer.RenderTarget {
+    method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(boolean cancelPending);
+    method public boolean isAttached();
+    method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender();
+    method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(int width, int height);
+  }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+  public final inline class EglConfigAttributes {
+    ctor public EglConfigAttributes();
+  }
+
+  public static final class EglConfigAttributes.Builder {
+    method public void include(int[] attributes);
+    method public infix void to(int, int that);
+  }
+
+  public final class EglConfigAttributesKt {
+    method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+    method public static int[] getEglConfigAttributes1010102();
+    method public static int[] getEglConfigAttributes8888();
+    method public static int[] getEglConfigAttributesF16();
+    property public static final int[] EglConfigAttributes1010102;
+    property public static final int[] EglConfigAttributes8888;
+    property public static final int[] EglConfigAttributesF16;
+    field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+    field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+    field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+  }
+
+  public final class EglException extends java.lang.RuntimeException {
+    ctor public EglException(int error, optional String msg);
+    method public int getError();
+    method public String getMsg();
+    property public final int error;
+    property public String message;
+    property public final String msg;
+  }
+
+  public final inline class EglExtensions {
+    ctor public EglExtensions();
+  }
+
+  public static final class EglExtensions.Companion {
+    method public java.util.Set<? extends java.lang.String> from(String queryString);
+  }
+
+  public final class EglExtensionsKt {
+    field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+    field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+    field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+    field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+    field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+    field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+    field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+    field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+    field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+    field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+    field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+    field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+    field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+    field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+  }
+
+  public final class EglManager {
+    ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+    method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface getCurrentDrawSurface();
+    method public android.opengl.EGLSurface getCurrentReadSurface();
+    method public android.opengl.EGLSurface getDefaultSurface();
+    method public android.opengl.EGLConfig? getEglConfig();
+    method public android.opengl.EGLContext? getEglContext();
+    method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+    method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+    method public void initialize();
+    method public boolean isExtensionSupported(String extensionName);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+    method public void release();
+    method public void swapAndFlushBuffers();
+    property public final android.opengl.EGLSurface currentDrawSurface;
+    property public final android.opengl.EGLSurface currentReadSurface;
+    property public final android.opengl.EGLSurface defaultSurface;
+    property public final android.opengl.EGLConfig? eglConfig;
+    property public final android.opengl.EGLContext? eglContext;
+    property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+    property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+    field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+  }
+
+  public static final class EglManager.Companion {
+  }
+
+  public interface EglSpec {
+    method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+    method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+    method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+    method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+    method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+    method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+    method public int eglGetError();
+    method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+    method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+    method public String eglQueryString(int nameId);
+    method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+    method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+    method public default String getErrorMessage();
+    method public default static String getStatusString(int error);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+  }
+
+  public static final class EglSpec.Companion {
+    method public String getStatusString(int error);
+  }
+
+  public final class EglVersion {
+    ctor public EglVersion(int major, int minor);
+    method public int component1();
+    method public int component2();
+    method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+    method public int getMajor();
+    method public int getMinor();
+    property public final int major;
+    property public final int minor;
+    field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+    field public static final androidx.graphics.opengl.egl.EglVersion V14;
+    field public static final androidx.graphics.opengl.egl.EglVersion V15;
+  }
+
+  public static final class EglVersion.Companion {
+  }
+
+}
+
+package androidx.graphics.surface {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+    ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+    method protected void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Companion {
+  }
+
+  public static class SurfaceControlCompat.Transaction {
+    ctor public SurfaceControlCompat.Transaction();
+    method public final void delete();
+    method public final void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Transaction.Companion {
+  }
+
+}
+
diff --git a/graphics/graphics-core/api/res-current.txt b/graphics/graphics-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/graphics/graphics-core/api/res-current.txt
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
new file mode 100644
index 0000000..4dce33b
--- /dev/null
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -0,0 +1,207 @@
+// Signature format: 4.0
+package androidx.graphics.opengl {
+
+  public final class GLRenderer {
+    ctor public GLRenderer(optional kotlin.jvm.functions.Function0<? extends androidx.graphics.opengl.egl.EglSpec> eglSpecFactory, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglManager,? extends android.opengl.EGLConfig> eglConfigFactory);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.Surface surface, int width, int height, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.SurfaceView surfaceView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public androidx.graphics.opengl.GLRenderer.RenderTarget attach(android.view.TextureView textureView, androidx.graphics.opengl.GLRenderer.RenderCallback renderer);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(androidx.graphics.opengl.GLRenderer.RenderTarget target, boolean cancelPending);
+    method public boolean isRunning();
+    method public void registerEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender(androidx.graphics.opengl.GLRenderer.RenderTarget target);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(androidx.graphics.opengl.GLRenderer.RenderTarget target, int width, int height);
+    method public void start(optional String name);
+    method public void start();
+    method public void stop(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer,kotlin.Unit>? onStop);
+    method public void stop(boolean cancelPending);
+    method public void unregisterEglContextCallback(androidx.graphics.opengl.GLRenderer.EglContextCallback callback);
+    field public static final androidx.graphics.opengl.GLRenderer.Companion Companion;
+  }
+
+  public static final class GLRenderer.Companion {
+  }
+
+  public static interface GLRenderer.EglContextCallback {
+    method @WorkerThread public void onEglContextCreated(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public void onEglContextDestroyed(androidx.graphics.opengl.egl.EglManager eglManager);
+  }
+
+  public static interface GLRenderer.RenderCallback {
+    method @WorkerThread public void onDrawFrame(androidx.graphics.opengl.egl.EglManager eglManager);
+    method @WorkerThread public default android.opengl.EGLSurface onSurfaceCreated(androidx.graphics.opengl.egl.EglSpec spec, android.opengl.EGLConfig config, android.view.Surface surface, int width, int height);
+  }
+
+  public static final class GLRenderer.RenderTarget {
+    method public void detach(boolean cancelPending, optional kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onDetachComplete);
+    method public void detach(boolean cancelPending);
+    method public boolean isAttached();
+    method public void requestRender(optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onRenderComplete);
+    method public void requestRender();
+    method public void resize(int width, int height, optional @WorkerThread kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.GLRenderer.RenderTarget,kotlin.Unit>? onResizeComplete);
+    method public void resize(int width, int height);
+  }
+
+}
+
+package androidx.graphics.opengl.egl {
+
+  public final inline class EglConfigAttributes {
+    ctor public EglConfigAttributes();
+  }
+
+  public static final class EglConfigAttributes.Builder {
+    ctor @kotlin.PublishedApi internal EglConfigAttributes.Builder();
+    method @kotlin.PublishedApi internal int[] build();
+    method public void include(int[] attributes);
+    method public infix void to(int, int that);
+  }
+
+  public final class EglConfigAttributesKt {
+    method public static inline int[] EglConfigAttributes(kotlin.jvm.functions.Function1<? super androidx.graphics.opengl.egl.EglConfigAttributes.Builder,kotlin.Unit> block);
+    method public static int[] getEglConfigAttributes1010102();
+    method public static int[] getEglConfigAttributes8888();
+    method public static int[] getEglConfigAttributesF16();
+    property public static final int[] EglConfigAttributes1010102;
+    property public static final int[] EglConfigAttributes8888;
+    property public static final int[] EglConfigAttributesF16;
+    field public static final int EglColorComponentTypeExt = 13113; // 0x3339
+    field public static final int EglColorComponentTypeFixedExt = 13114; // 0x333a
+    field public static final int EglColorComponentTypeFloatExt = 13115; // 0x333b
+  }
+
+  public final class EglException extends java.lang.RuntimeException {
+    ctor public EglException(int error, optional String msg);
+    method public int getError();
+    method public String getMsg();
+    property public final int error;
+    property public String message;
+    property public final String msg;
+  }
+
+  public final inline class EglExtensions {
+    ctor public EglExtensions();
+  }
+
+  public static final class EglExtensions.Companion {
+    method public java.util.Set<? extends java.lang.String> from(String queryString);
+  }
+
+  public final class EglExtensionsKt {
+    field public static final String EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync";
+    field public static final String EglExtBufferAge = "EGL_EXT_buffer_age";
+    field public static final String EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough";
+    field public static final String EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq";
+    field public static final String EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb";
+    field public static final String EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float";
+    field public static final String EglImgContextPriority = "EGL_IMG_context_priority";
+    field public static final String EglKhrFenceSync = "EGL_KHR_fence_sync";
+    field public static final String EglKhrGlColorSpace = "EGL_KHR_gl_colorspace";
+    field public static final String EglKhrNoConfigContext = "EGL_KHR_no_config_context";
+    field public static final String EglKhrPartialUpdate = "EGL_KHR_partial_update";
+    field public static final String EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context";
+    field public static final String EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage";
+    field public static final String EglKhrWaitSync = "EGL_KHR_wait_sync";
+  }
+
+  public final class EglManager {
+    ctor public EglManager(optional androidx.graphics.opengl.egl.EglSpec eglSpec);
+    method public android.opengl.EGLContext createContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface getCurrentDrawSurface();
+    method public android.opengl.EGLSurface getCurrentReadSurface();
+    method public android.opengl.EGLSurface getDefaultSurface();
+    method public android.opengl.EGLConfig? getEglConfig();
+    method public android.opengl.EGLContext? getEglContext();
+    method public androidx.graphics.opengl.egl.EglSpec getEglSpec();
+    method public androidx.graphics.opengl.egl.EglVersion getEglVersion();
+    method public void initialize();
+    method public boolean isExtensionSupported(String extensionName);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface, optional android.opengl.EGLSurface readSurface);
+    method public boolean makeCurrent(android.opengl.EGLSurface drawSurface);
+    method public void release();
+    method public void swapAndFlushBuffers();
+    property public final android.opengl.EGLSurface currentDrawSurface;
+    property public final android.opengl.EGLSurface currentReadSurface;
+    property public final android.opengl.EGLSurface defaultSurface;
+    property public final android.opengl.EGLConfig? eglConfig;
+    property public final android.opengl.EGLContext? eglContext;
+    property public final androidx.graphics.opengl.egl.EglSpec eglSpec;
+    property public final androidx.graphics.opengl.egl.EglVersion eglVersion;
+    field public static final androidx.graphics.opengl.egl.EglManager.Companion Companion;
+  }
+
+  public static final class EglManager.Companion {
+  }
+
+  public interface EglSpec {
+    method public android.opengl.EGLContext eglCreateContext(android.opengl.EGLConfig config);
+    method public android.opengl.EGLSurface eglCreatePBufferSurface(android.opengl.EGLConfig config, int[]? configAttributes);
+    method public android.opengl.EGLSurface eglCreateWindowSurface(android.opengl.EGLConfig config, android.view.Surface surface, int[]? configAttributes);
+    method public void eglDestroyContext(android.opengl.EGLContext eglContext);
+    method public boolean eglDestroySurface(android.opengl.EGLSurface surface);
+    method public android.opengl.EGLSurface eglGetCurrentDrawSurface();
+    method public android.opengl.EGLSurface eglGetCurrentReadSurface();
+    method public int eglGetError();
+    method public androidx.graphics.opengl.egl.EglVersion eglInitialize();
+    method public boolean eglMakeCurrent(android.opengl.EGLContext context, android.opengl.EGLSurface drawSurface, android.opengl.EGLSurface readSurface);
+    method public String eglQueryString(int nameId);
+    method public boolean eglQuerySurface(android.opengl.EGLSurface surface, int attribute, int[] result, int offset);
+    method public boolean eglSwapBuffers(android.opengl.EGLSurface surface);
+    method public default String getErrorMessage();
+    method public default static String getStatusString(int error);
+    method public android.opengl.EGLConfig? loadConfig(int[] configAttributes);
+    field public static final androidx.graphics.opengl.egl.EglSpec.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglSpec Egl14;
+  }
+
+  public static final class EglSpec.Companion {
+    method public String getStatusString(int error);
+  }
+
+  public final class EglVersion {
+    ctor public EglVersion(int major, int minor);
+    method public int component1();
+    method public int component2();
+    method public androidx.graphics.opengl.egl.EglVersion copy(int major, int minor);
+    method public int getMajor();
+    method public int getMinor();
+    property public final int major;
+    property public final int minor;
+    field public static final androidx.graphics.opengl.egl.EglVersion.Companion Companion;
+    field public static final androidx.graphics.opengl.egl.EglVersion Unknown;
+    field public static final androidx.graphics.opengl.egl.EglVersion V14;
+    field public static final androidx.graphics.opengl.egl.EglVersion V15;
+  }
+
+  public static final class EglVersion.Companion {
+  }
+
+}
+
+package androidx.graphics.surface {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.Q) public final class SurfaceControlCompat {
+    ctor public SurfaceControlCompat(android.view.Surface surface, String debugName);
+    method protected void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Companion {
+  }
+
+  public static class SurfaceControlCompat.Transaction {
+    ctor public SurfaceControlCompat.Transaction();
+    method public final void delete();
+    method public final void finalize();
+    field public static final androidx.graphics.surface.SurfaceControlCompat.Transaction.Companion Companion;
+  }
+
+  public static final class SurfaceControlCompat.Transaction.Companion {
+  }
+
+}
+
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
new file mode 100644
index 0000000..c421162
--- /dev/null
+++ b/graphics/graphics-core/build.gradle
@@ -0,0 +1,59 @@
+/*
+ * 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.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    implementation 'androidx.annotation:annotation:1.2.0'
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 17
+        externalNativeBuild {
+            cmake {
+                cppFlags ''
+            }
+        }
+    }
+    ndkVersion "23.1.7779620"
+    externalNativeBuild {
+        cmake {
+            path file('src/main/cpp/CMakeLists.txt')
+            version '3.22.1'
+        }
+    }
+}
+
+androidx {
+    name = "Android Graphics Core"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.GRAPHICS
+    inceptionYear = "2021"
+    description = "Leverage graphics facilities across multiple Android platform releases"
+}
diff --git a/graphics/graphics-core/lint-baseline.xml b/graphics/graphics-core/lint-baseline.xml
new file mode 100644
index 0000000..5bcb65c
--- /dev/null
+++ b/graphics/graphics-core/lint-baseline.xml
@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 7.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta02)" variant="all" version="7.1.0-beta02">
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testConfig8888() {"
+        errorLine2="        ~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+            line="30"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testConfig1010102() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+            line="47"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testConfigF16() {"
+        errorLine2="        ~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+            line="63"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testInclude() {"
+        errorLine2="        ~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt"
+            line="80"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testSupportsBufferAge() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="28"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testSupportBufferAgeFromPartialUpdate() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="33"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testSetDamage() {"
+        errorLine2="        ~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="41"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testSwapBuffersWithDamage() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="49"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testColorSpace() {"
+        errorLine2="        ~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="57"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testNoConfigContext() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="65"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testPixelFormatFloat() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="73"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testScRgb() {"
+        errorLine2="        ~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="81"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testDisplayP3() {"
+        errorLine2="        ~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="89"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testHDR() {"
+        errorLine2="        ~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="97"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testContextPriority() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="105"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testSurfacelessContext() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="113"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testFenceSync() {"
+        errorLine2="        ~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="121"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testWaitSync() {"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="128"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testNativeFenceSync() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="133"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testExtensionsQueryStringParsing() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt"
+            line="141"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testDestructuringComponents() {"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+            line="28"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testEquals() {"
+        errorLine2="        ~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+            line="35"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testToString() {"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+            line="40"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="MissingTestSizeAnnotation"
+        message="Missing test size annotation"
+        errorLine1="    fun testHashCode() {"
+        errorLine2="        ~~~~~~~~~~~~">
+        <location
+            file="src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt"
+            line="45"
+            column="9"/>
+    </issue>
+
+</issues>
diff --git a/graphics/graphics-core/src/androidTest/AndroidManifest.xml b/graphics/graphics-core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..141b3c9
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/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.graphics.core.test">
+
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+    <supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
+    <supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
+    <application>
+        <activity android:name="androidx.graphics.opengl.egl.EglTestActivity"
+            android:label="Graphics Core Test"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name="androidx.graphics.opengl.GLTestActivity"
+            android:label="Graphics Core Test"
+            android:exported="true">
+        </activity>
+    </application>
+</manifest>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
new file mode 100644
index 0000000..e2a0a96
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -0,0 +1,702 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.graphics.opengl
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.media.Image
+import android.media.ImageReader
+import android.opengl.EGL14
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.view.PixelCopy
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import androidx.lifecycle.Lifecycle.State
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GLRendererTest {
+
+    @Test
+    fun testStartAfterStop() {
+        with(GLRenderer()) {
+            start("thread1")
+            stop(true)
+            start("thread2")
+            stop(true)
+        }
+    }
+
+    @Test
+    fun testAttachBeforeStartThrows() {
+        try {
+            with(GLRenderer()) {
+                attach(
+                    Surface(SurfaceTexture(17)),
+                    10,
+                    10,
+                    object : GLRenderer.RenderCallback {
+                    override fun onDrawFrame(eglManager: EglManager) {
+                        // NO-OP
+                    }
+                })
+            }
+            fail("Start should be called first")
+        } catch (exception: IllegalStateException) {
+            // Success, attach before call to start should fail
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testRender() {
+        val latch = CountDownLatch(1)
+        val renderer = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val width = 5
+        val height = 8
+        val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target = glRenderer.attach(reader.surface, width, height, renderer)
+        target.requestRender {
+            latch.countDown()
+        }
+
+        assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        val plane = reader.acquireLatestImage().planes[0]
+        assertEquals(4, plane.pixelStride)
+
+        val targetColor = Color.argb(255, 255, 0, 255)
+        Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+        target.detach(true)
+
+        glRenderer.stop(true)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testDetachExecutesPendingRequests() {
+        val latch = CountDownLatch(1)
+        val renderer = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val width = 5
+        val height = 8
+        val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target = glRenderer.attach(reader.surface, width, height, renderer)
+        target.requestRender {
+            latch.countDown()
+        }
+        target.detach(false) // RequestRender Call should still execute
+
+        assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        val plane = reader.acquireLatestImage().planes[0]
+        assertEquals(4, plane.pixelStride)
+
+        val targetColor = Color.argb(255, 255, 0, 255)
+        Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+        glRenderer.stop(true)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testStopExecutesPendingRequests() {
+        val latch = CountDownLatch(1)
+        val surfaceWidth = 5
+        val surfaceHeight = 8
+        val renderer = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+                assertEquals(surfaceWidth, size.width)
+                assertEquals(surfaceHeight, size.height)
+                GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val reader = ImageReader.newInstance(surfaceWidth, surfaceHeight, PixelFormat.RGBA_8888, 1)
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target = glRenderer.attach(reader.surface, surfaceWidth, surfaceHeight, renderer)
+        target.requestRender {
+            latch.countDown()
+        }
+        glRenderer.stop(false) // RequestRender call should still execute
+
+        assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        val plane = reader.acquireLatestImage().planes[0]
+        assertEquals(4, plane.pixelStride)
+
+        val targetColor = Color.argb(255, 255, 0, 255)
+        Api19Helpers.verifyPlaneContent(surfaceWidth, surfaceHeight, plane, targetColor)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testDetachExecutesMultiplePendingRequests() {
+        val numRenders = 4
+        val latch = CountDownLatch(numRenders)
+        val renderCount = AtomicInteger(0)
+        val renderer = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                var red: Float = 0f
+                var green: Float = 0f
+                var blue: Float = 0f
+                when (renderCount.get()) {
+                    1 -> {
+                        red = 1f
+                    }
+                    2 -> {
+                        green = 1f
+                    }
+                    3 -> {
+                        blue = 1f
+                    }
+                }
+                GLES20.glClearColor(red, green, blue, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val width = 5
+        val height = 8
+        val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target = glRenderer.attach(reader.surface, width, height, renderer)
+        // Issuing multiple requestRender calls to ensure each of them are
+        // executed even when a detach call is made
+        repeat(numRenders) {
+            target.requestRender {
+                renderCount.incrementAndGet()
+                latch.countDown()
+            }
+        }
+
+        target.detach(false) // RequestRender calls should still execute
+
+        assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(numRenders, renderCount.get())
+        val plane = reader.acquireLatestImage().planes[0]
+        assertEquals(4, plane.pixelStride)
+
+        val targetColor = Color.argb(255, 0, 0, 255)
+        Api19Helpers.verifyPlaneContent(width, height, plane, targetColor)
+
+        glRenderer.stop(true)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testDetachCancelsPendingRequests() {
+        val latch = CountDownLatch(1)
+        val renderer = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val width = 5
+        val height = 8
+        val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target = glRenderer.attach(reader.surface, width, height, renderer)
+        target.requestRender {
+            latch.countDown()
+        }
+        target.detach(false) // RequestRender Call should be cancelled
+
+        glRenderer.stop(true)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    fun testMultipleAttachedSurfaces() {
+        val latch = CountDownLatch(2)
+        val renderer1 = object : GLRenderer.RenderCallback {
+
+            override fun onDrawFrame(eglManager: EglManager) {
+                GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val renderer2 = object : GLRenderer.RenderCallback {
+            override fun onDrawFrame(eglManager: EglManager) {
+                GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            }
+        }
+
+        val width1 = 6
+        val height1 = 7
+
+        val width2 = 11
+        val height2 = 23
+        val reader1 = ImageReader.newInstance(width1, height1, PixelFormat.RGBA_8888, 1)
+
+        val reader2 = ImageReader.newInstance(width2, height2, PixelFormat.RGBA_8888, 1)
+
+        val glRenderer = GLRenderer()
+        glRenderer.start()
+
+        val target1 = glRenderer.attach(reader1.surface, width1, height1, renderer1)
+        val target2 = glRenderer.attach(reader2.surface, width2, height2, renderer2)
+        target1.requestRender {
+            latch.countDown()
+        }
+        target2.requestRender {
+            latch.countDown()
+        }
+
+        assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+        val plane1 = reader1.acquireLatestImage().planes[0]
+        val plane2 = reader2.acquireLatestImage().planes[0]
+
+        Api19Helpers.verifyPlaneContent(width1, height1, plane1, Color.argb(255, 255, 0, 0))
+        Api19Helpers.verifyPlaneContent(width2, height2, plane2, Color.argb(255, 0, 0, 255))
+
+        target1.detach(true)
+        target2.detach(true)
+
+        val attachLatch = CountDownLatch(1)
+        glRenderer.stop(true) {
+            attachLatch.countDown()
+        }
+
+        assertTrue(attachLatch.await(3000, TimeUnit.MILLISECONDS))
+    }
+
+    /**
+     * Helper class for test methods that refer to APIs that may not exist on earlier API levels.
+     * This must be broken out into a separate class instead of being defined within the
+     * test class as the test runner will inspect all methods + parameter types in advance.
+     * If a parameter type does not exist on a particular API level, it will crash even if
+     * there are corresponding @SdkSuppress and @RequiresApi
+     * See https://b.corp.google.com/issues/221485597
+     */
+    class Api19Helpers private constructor() {
+        companion object {
+            @RequiresApi(Build.VERSION_CODES.KITKAT)
+            fun verifyPlaneContent(width: Int, height: Int, plane: Image.Plane, targetColor: Int) {
+                val rowPadding = plane.rowStride - plane.pixelStride * width
+                var offset = 0
+                for (y in 0 until height) {
+                    for (x in 0 until width) {
+                        val red = plane.buffer[offset].toInt() and 0xff
+                        val green = plane.buffer[offset + 1].toInt() and 0xff
+                        val blue = plane.buffer[offset + 2].toInt() and 0xff
+                        val alpha = plane.buffer[offset + 3].toInt() and 0xff
+                        val packedColor = Color.argb(alpha, red, green, blue)
+                        assertEquals("Index: $x, $y", targetColor, packedColor)
+                        offset += plane.pixelStride
+                    }
+                    offset += rowPadding
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testNonStartedGLRendererIsNotRunning() {
+        assertFalse(GLRenderer().isRunning())
+    }
+
+    @Test
+    fun testRepeatedStartAndStopRunningState() {
+        val glRenderer = GLRenderer()
+        assertFalse(glRenderer.isRunning())
+        glRenderer.start()
+        assertTrue(glRenderer.isRunning())
+        glRenderer.stop(true)
+        assertFalse(glRenderer.isRunning())
+        glRenderer.start()
+        assertTrue(glRenderer.isRunning())
+        glRenderer.stop(true)
+        assertFalse(glRenderer.isRunning())
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    fun testSurfaceViewAttach() {
+        withGLTestActivity {
+            assertNotNull(surfaceView)
+
+            val latch = CountDownLatch(1)
+            val glRenderer = GLRenderer().apply { start() }
+            val target = glRenderer.attach(surfaceView, ColorRenderCallback(Color.BLUE))
+
+            target.requestRender {
+                latch.countDown()
+            }
+
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+
+            val bitmap = Bitmap.createBitmap(
+                GLTestActivity.TARGET_WIDTH,
+                GLTestActivity.TARGET_HEIGHT,
+                Bitmap.Config.ARGB_8888
+            )
+
+            blockingPixelCopy(bitmap) { surfaceView.holder.surface }
+
+            assertTrue(bitmap.isAllColor(Color.BLUE))
+
+            val stopLatch = CountDownLatch(1)
+            glRenderer.stop(true) {
+                stopLatch.countDown()
+            }
+
+            assertTrue(stopLatch.await(3000, TimeUnit.MILLISECONDS))
+            // Assert that targets are detached when the GLRenderer is stopped
+            assertFalse(target.isAttached())
+        }
+    }
+
+    @Test
+    fun testTextureViewOnResizeCalled() {
+        withGLTestActivity {
+            assertNotNull(textureView)
+            val glRenderer = GLRenderer().apply { start() }
+
+            val resizeLatch = CountDownLatch(1)
+            val target = glRenderer.attach(textureView, object : GLRenderer.RenderCallback {
+                override fun onDrawFrame(eglManager: EglManager) {
+                    val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+                    assertTrue(size.width > 0)
+                    assertTrue(size.height > 0)
+                    resizeLatch.countDown()
+                }
+            })
+            target.requestRender()
+
+            assertTrue(resizeLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val detachLatch = CountDownLatch(1)
+            target.detach(false) {
+                detachLatch.countDown()
+            }
+            assertTrue(detachLatch.await(3000, TimeUnit.MILLISECONDS))
+            glRenderer.stop(true)
+        }
+    }
+
+    @Test
+    fun testSurfaceViewOnResizeCalled() {
+        withGLTestActivity {
+            assertNotNull(surfaceView)
+            val glRenderer = GLRenderer().apply { start() }
+
+            val resizeLatch = CountDownLatch(1)
+            val target = glRenderer.attach(surfaceView, object : GLRenderer.RenderCallback {
+                override fun onDrawFrame(eglManager: EglManager) {
+                    val size = eglManager.eglSpec.querySurfaceSize(eglManager.currentDrawSurface)
+                    assertTrue(size.width > 0)
+                    assertTrue(size.height > 0)
+                    resizeLatch.countDown()
+                }
+            })
+            target.requestRender()
+
+            assertTrue(resizeLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val detachLatch = CountDownLatch(1)
+            target.detach(false) {
+                detachLatch.countDown()
+            }
+            assertTrue(detachLatch.await(3000, TimeUnit.MILLISECONDS))
+            glRenderer.stop(true)
+        }
+    }
+
+    data class Size(val width: Int, val height: Int)
+
+    fun EglSpec.querySurfaceSize(eglSurface: EGLSurface): Size {
+        val result = IntArray(1)
+        eglQuerySurface(
+            eglSurface, EGL14.EGL_WIDTH, result, 0)
+        val width = result[0]
+        eglQuerySurface(
+            eglSurface, EGL14.EGL_HEIGHT, result, 0)
+        val height = result[0]
+        return Size(width, height)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    fun testTextureViewAttach() {
+        withGLTestActivity {
+            assertNotNull(textureView)
+
+            val latch = CountDownLatch(1)
+            val glRenderer = GLRenderer().apply { start() }
+            val target = glRenderer.attach(textureView, ColorRenderCallback(Color.BLUE))
+            target.requestRender {
+                latch.countDown()
+            }
+            assertTrue(latch.await(3000, TimeUnit.MILLISECONDS))
+
+            val bitmap = Bitmap.createBitmap(
+                GLTestActivity.TARGET_WIDTH,
+                GLTestActivity.TARGET_HEIGHT,
+                Bitmap.Config.ARGB_8888
+            )
+
+            blockingPixelCopy(bitmap) { Surface(textureView.surfaceTexture) }
+            assertTrue(bitmap.isAllColor(Color.BLUE))
+
+            val stopLatch = CountDownLatch(1)
+            glRenderer.stop(true) {
+                stopLatch.countDown()
+            }
+
+            assertTrue(stopLatch.await(3000, TimeUnit.MILLISECONDS))
+            // Assert that targets are detached when the GLRenderer is stopped
+            assertFalse(target.isAttached())
+        }
+    }
+
+    @Test
+    fun testEglContextCallbackInvoked() {
+        val createdLatch = CountDownLatch(1)
+        val destroyedLatch = CountDownLatch(1)
+        val createCount = AtomicInteger()
+        val destroyCount = AtomicInteger()
+        val callback = object : GLRenderer.EglContextCallback {
+
+            override fun onEglContextCreated(eglManager: EglManager) {
+                createCount.incrementAndGet()
+                createdLatch.countDown()
+            }
+
+            override fun onEglContextDestroyed(eglManager: EglManager) {
+                destroyCount.incrementAndGet()
+                destroyedLatch.countDown()
+            }
+        }
+
+        val glRenderer = GLRenderer().apply { start() }
+        glRenderer.registerEglContextCallback(callback)
+
+        glRenderer.attach(
+            Surface(SurfaceTexture(12)),
+            10,
+            10,
+            ColorRenderCallback(Color.RED)
+        ).requestRender()
+
+        assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(1, createCount.get())
+
+        glRenderer.stop(true)
+
+        assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(1, destroyCount.get())
+    }
+
+    @Test
+    fun testEglContextCallbackInvokedBeforeStart() {
+        val createdLatch = CountDownLatch(1)
+        val destroyedLatch = CountDownLatch(1)
+        val createCount = AtomicInteger()
+        val destroyCount = AtomicInteger()
+        val callback = object : GLRenderer.EglContextCallback {
+
+            override fun onEglContextCreated(eglManager: EglManager) {
+                createCount.incrementAndGet()
+                createdLatch.countDown()
+            }
+
+            override fun onEglContextDestroyed(eglManager: EglManager) {
+                destroyCount.incrementAndGet()
+                destroyedLatch.countDown()
+            }
+        }
+
+        val glRenderer = GLRenderer()
+        // Adding a callback before the glRenderer is started should still
+        // deliver onEglRendererCreated callbacks
+        glRenderer.registerEglContextCallback(callback)
+        glRenderer.start()
+
+        glRenderer.attach(
+            Surface(SurfaceTexture(12)),
+            10,
+            10,
+            ColorRenderCallback(Color.CYAN)
+        ).requestRender()
+
+        assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(1, createCount.get())
+
+        glRenderer.stop(true)
+
+        assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(1, destroyCount.get())
+    }
+
+    @Test
+    fun testEglContextCallbackRemove() {
+        val createdLatch = CountDownLatch(1)
+        val destroyedLatch = CountDownLatch(1)
+        val createCount = AtomicInteger()
+        val destroyCount = AtomicInteger()
+        val callback = object : GLRenderer.EglContextCallback {
+
+            override fun onEglContextCreated(eglManager: EglManager) {
+                createCount.incrementAndGet()
+                createdLatch.countDown()
+            }
+
+            override fun onEglContextDestroyed(eglManager: EglManager) {
+                destroyCount.incrementAndGet()
+            }
+        }
+
+        val glRenderer = GLRenderer()
+        // Adding a callback before the glRenderer is started should still
+        // deliver onEglRendererCreated callbacks
+        glRenderer.registerEglContextCallback(callback)
+        glRenderer.start()
+
+        glRenderer.attach(
+            Surface(SurfaceTexture(12)),
+            10,
+            10,
+            ColorRenderCallback(Color.CYAN)
+        ).requestRender()
+
+        assertTrue(createdLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(1, createCount.get())
+
+        glRenderer.unregisterEglContextCallback(callback)
+
+        glRenderer.stop(false) {
+            destroyedLatch.countDown()
+        }
+
+        assertTrue(destroyedLatch.await(3000, TimeUnit.MILLISECONDS))
+        assertEquals(0, destroyCount.get())
+    }
+
+    /**
+     * Helper method to create a GLTestActivity instance and progress it through the Activity
+     * lifecycle to the resumed state so we can issue rendering commands into the corresponding
+     * SurfaceView/TextureView
+     */
+    private fun withGLTestActivity(block: GLTestActivity.() -> Unit) {
+        ActivityScenario.launch(GLTestActivity::class.java).moveToState(State.RESUMED).onActivity {
+            block(it!!)
+        }
+    }
+
+    /**
+     * Helper RenderCallback that renders a solid color and invokes the provided CountdownLatch
+     * when rendering is complete
+     */
+    private class ColorRenderCallback(
+        val targetColor: Int
+    ) : GLRenderer.RenderCallback {
+
+        override fun onDrawFrame(eglManager: EglManager) {
+            GLES20.glClearColor(
+                Color.red(targetColor) / 255f,
+                Color.green(targetColor) / 255f,
+                Color.blue(targetColor) / 255f,
+                Color.alpha(targetColor) / 255f,
+            )
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        }
+    }
+
+    /**
+     * Helper method that synchronously blocks until the PixelCopy operation is complete
+     */
+    @RequiresApi(Build.VERSION_CODES.N)
+    private fun blockingPixelCopy(
+        destBitmap: Bitmap,
+        surfaceProvider: () -> Surface
+    ) {
+        val copyLatch = CountDownLatch(1)
+        val copyThread = HandlerThread("copyThread").apply { start() }
+        val copyHandler = Handler(copyThread.looper)
+        PixelCopy.request(surfaceProvider.invoke(),
+            destBitmap,
+            { copyResult ->
+                assertEquals(PixelCopy.SUCCESS, copyResult)
+                copyLatch.countDown()
+                copyThread.quit()
+            },
+            copyHandler
+        )
+        assertTrue(copyLatch.await(3000, TimeUnit.MILLISECONDS))
+    }
+
+    private fun Bitmap.isAllColor(targetColor: Int): Boolean {
+        for (i in 0 until width) {
+            for (j in 0 until height) {
+                if (getPixel(i, j) != targetColor) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt
new file mode 100644
index 0000000..103c36b
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLTestActivity.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.graphics.opengl
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.SurfaceView
+import android.view.TextureView
+import android.widget.LinearLayout
+
+class GLTestActivity : Activity() {
+
+    companion object {
+        const val TARGET_WIDTH = 30
+        const val TARGET_HEIGHT = 20
+    }
+
+    lateinit var surfaceView: SurfaceView
+    lateinit var textureView: TextureView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        surfaceView = SurfaceView(this)
+        textureView = TextureView(this)
+        val ll = LinearLayout(this).apply {
+            orientation = LinearLayout.VERTICAL
+            weightSum = 2f
+        }
+        val layoutParams = LinearLayout.LayoutParams(TARGET_WIDTH, TARGET_HEIGHT)
+
+        ll.addView(surfaceView, layoutParams)
+        ll.addView(textureView, layoutParams)
+
+        setContentView(ll)
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt
new file mode 100644
index 0000000..5a5ea46
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglConfigAttributesTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.graphics.opengl.egl
+
+import android.opengl.EGL14
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglConfigAttributesTest {
+
+    @Test
+    fun testConfig8888() {
+        with(EglConfigAttributes8888.attrs) {
+            assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+            assertTrue(find(EGL14.EGL_RED_SIZE, 8))
+            assertTrue(find(EGL14.EGL_GREEN_SIZE, 8))
+            assertTrue(find(EGL14.EGL_BLUE_SIZE, 8))
+            assertTrue(find(EGL14.EGL_ALPHA_SIZE, 8))
+            assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+            assertTrue(find(EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE))
+            assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+            assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+            assertEquals(this[size - 1], EGL14.EGL_NONE)
+            assertEquals(19, size)
+        }
+    }
+
+    @Test
+    fun testConfig1010102() {
+        with(EglConfigAttributes1010102.attrs) {
+            assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+            assertTrue(find(EGL14.EGL_RED_SIZE, 10))
+            assertTrue(find(EGL14.EGL_GREEN_SIZE, 10))
+            assertTrue(find(EGL14.EGL_BLUE_SIZE, 10))
+            assertTrue(find(EGL14.EGL_ALPHA_SIZE, 2))
+            assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+            assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+            assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+            assertEquals(this[size - 1], EGL14.EGL_NONE)
+            assertEquals(17, size)
+        }
+    }
+
+    @Test
+    fun testConfigF16() {
+        with(EglConfigAttributesF16.attrs) {
+            assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+            assertTrue(find(EglColorComponentTypeExt, EglColorComponentTypeFloatExt))
+            assertTrue(find(EGL14.EGL_RED_SIZE, 16))
+            assertTrue(find(EGL14.EGL_GREEN_SIZE, 16))
+            assertTrue(find(EGL14.EGL_BLUE_SIZE, 16))
+            assertTrue(find(EGL14.EGL_ALPHA_SIZE, 16))
+            assertTrue(find(EGL14.EGL_DEPTH_SIZE, 0))
+            assertTrue(find(EGL14.EGL_STENCIL_SIZE, 0))
+            assertTrue(find(EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT))
+            assertEquals(this[size - 1], EGL14.EGL_NONE)
+            assertEquals(19, size)
+        }
+    }
+
+    @Test
+    fun testInclude() {
+        // Verify that custom config that uses an include initially and overwrites
+        // individual values is handled appropriately even if the config is technically invalid
+        val customConfig = EglConfigAttributes {
+            include(EglConfigAttributes8888)
+            EGL14.EGL_RED_SIZE to 27
+            EglColorComponentTypeExt to EglColorComponentTypeFloatExt
+            EGL14.EGL_STENCIL_SIZE to 32
+        }
+
+        with(customConfig.attrs) {
+            assertTrue(find(EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT))
+            assertTrue(find(EGL14.EGL_RED_SIZE, 27))
+            assertTrue(find(EglColorComponentTypeExt, EglColorComponentTypeFloatExt))
+            assertTrue(find(EGL14.EGL_STENCIL_SIZE, 32))
+            assertEquals(this[size - 1], EGL14.EGL_NONE)
+            assertEquals(21, size)
+        }
+    }
+
+    private fun IntArray.find(key: Int, value: Int): Boolean {
+        // size - 1 to skip trailing EGL_NONE
+        for (i in 0 until this.size - 1 step 2) {
+            if (this[i] == key) {
+                return this[i + 1] == value
+            }
+        }
+        return false
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt
new file mode 100644
index 0000000..7396807
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglExtensionsTest.kt
@@ -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.graphics.opengl.egl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglExtensionsTest {
+
+    @Test
+    fun testSupportsBufferAge() {
+        assertTrue(EglExtensions(setOf("EGL_EXT_buffer_age")).isExtensionSupported(EglExtBufferAge))
+    }
+
+    @Test
+    fun testSupportBufferAgeFromPartialUpdate() {
+        // Buffer age can be supported from either EGL_EXT_buffer_age or EGL_KHR_partial_update
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_partial_update")).isExtensionSupported(EglKhrPartialUpdate)
+        )
+    }
+
+    @Test
+    fun testSetDamage() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_partial_update"))
+                .isExtensionSupported(EglKhrPartialUpdate)
+        )
+    }
+
+    @Test
+    fun testSwapBuffersWithDamage() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_swap_buffers_with_damage"))
+                .isExtensionSupported(EglKhrSwapBuffersWithDamage)
+        )
+    }
+
+    @Test
+    fun testColorSpace() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_gl_colorspace"))
+                .isExtensionSupported(EglKhrGlColorSpace)
+        )
+    }
+
+    @Test
+    fun testNoConfigContext() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_no_config_context"))
+                .isExtensionSupported(EglKhrNoConfigContext)
+        )
+    }
+
+    @Test
+    fun testPixelFormatFloat() {
+        assertTrue(
+            EglExtensions(setOf("EGL_EXT_pixel_format_float"))
+                .isExtensionSupported(EglExtPixelFormatFloat)
+        )
+    }
+
+    @Test
+    fun testScRgb() {
+        assertTrue(
+            EglExtensions(setOf("EGL_EXT_gl_colorspace_scrgb"))
+                .isExtensionSupported(EglExtGlColorSpaceScRgb)
+        )
+    }
+
+    @Test
+    fun testDisplayP3() {
+        assertTrue(
+            EglExtensions(setOf("EGL_EXT_gl_colorspace_display_p3_passthrough"))
+                .isExtensionSupported(EglExtColorSpaceDisplayP3Passthrough)
+        )
+    }
+
+    @Test
+    fun testHDR() {
+        assertTrue(
+            EglExtensions(setOf("EGL_EXT_gl_colorspace_bt2020_pq"))
+                .isExtensionSupported(EglExtGlColorSpaceBt2020Pq)
+        )
+    }
+
+    @Test
+    fun testContextPriority() {
+        assertTrue(
+            EglExtensions(setOf("EGL_IMG_context_priority"))
+                .isExtensionSupported(EglImgContextPriority)
+        )
+    }
+
+    @Test
+    fun testSurfacelessContext() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_surfaceless_context"))
+                .isExtensionSupported(EglKhrSurfacelessContext)
+        )
+    }
+
+    @Test
+    fun testFenceSync() {
+        assertTrue(
+            EglExtensions(setOf("EGL_KHR_fence_sync")).isExtensionSupported(EglKhrFenceSync)
+        )
+    }
+
+    @Test
+    fun testWaitSync() {
+        assertTrue(EglExtensions(setOf("EGL_KHR_wait_sync")).isExtensionSupported(EglKhrWaitSync))
+    }
+
+    @Test
+    fun testNativeFenceSync() {
+        assertTrue(
+            EglExtensions(setOf("EGL_ANDROID_native_fence_sync"))
+                .isExtensionSupported(EglAndroidNativeFenceSync)
+        )
+    }
+
+    @Test
+    fun testExtensionsQueryStringParsing() {
+        val extensionQuery = "EGL_EXT_buffer_age " +
+            "EGL_KHR_partial_update " +
+            "EGL_KHR_swap_buffers_with_damage " +
+            "EGL_KHR_gl_colorspace " +
+            "EGL_KHR_no_config_context " +
+            "EGL_EXT_pixel_format_float " +
+            "EGL_EXT_gl_colorspace_scrgb " +
+            "EGL_EXT_gl_colorspace_display_p3_passthrough " +
+            "EGL_EXT_gl_colorspace_bt2020_pq " +
+            "EGL_IMG_context_priority " +
+            "EGL_KHR_surfaceless_context " +
+            "EGL_KHR_fence_sync " +
+            "EGL_KHR_wait_sync " +
+            "EGL_ANDROID_native_fence_sync "
+        with(EglExtensions.from(extensionQuery)) {
+            assertTrue(isExtensionSupported(EglExtBufferAge))
+            assertTrue(isExtensionSupported(EglKhrPartialUpdate))
+            assertTrue(isExtensionSupported(EglKhrSwapBuffersWithDamage))
+            assertTrue(isExtensionSupported(EglKhrGlColorSpace))
+            assertTrue(isExtensionSupported(EglKhrNoConfigContext))
+            assertTrue(isExtensionSupported(EglExtPixelFormatFloat))
+            assertTrue(isExtensionSupported(EglExtGlColorSpaceScRgb))
+            assertTrue(isExtensionSupported(EglExtColorSpaceDisplayP3Passthrough))
+            assertTrue(isExtensionSupported(EglExtGlColorSpaceBt2020Pq))
+            assertTrue(isExtensionSupported(EglImgContextPriority))
+            assertTrue(isExtensionSupported(EglKhrSurfacelessContext))
+            assertTrue(isExtensionSupported(EglKhrFenceSync))
+            assertTrue(isExtensionSupported(EglKhrWaitSync))
+            assertTrue(isExtensionSupported(EglAndroidNativeFenceSync))
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt
new file mode 100644
index 0000000..e2ff28a6
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglManagerTest.kt
@@ -0,0 +1,394 @@
+/*
+ * 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.graphics.opengl.egl
+
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.media.ImageReader
+import android.opengl.EGL14
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.concurrent.thread
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EglManagerTest {
+
+    @Test
+    fun testInitializeAndRelease() {
+        testEglManager {
+            initialize()
+            val config = loadConfig(EglConfigAttributes8888)?.also {
+                createContext(it)
+            }
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+            // Even though EGL v 1.5 was introduced in API level 29 not all devices will advertise
+            // support for it. However, all devices should at least support EGL v 1.4
+            assertTrue(
+                "Unexpected EGL version, received $eglVersion",
+                eglVersion == EglVersion.V14 || eglVersion == EglVersion.V15
+            )
+            assertNotNull(eglContext)
+            assertNotNull(eglConfig)
+        }
+    }
+
+    @Test
+    fun testMultipleInitializeCallsIgnored() {
+        testEglManager {
+            initialize()
+            loadConfig(EglConfigAttributes8888)?.also {
+                createContext(it)
+            }
+            val currentContext = eglContext
+            val currentConfig = eglConfig
+            assertNotEquals(EGL14.EGL_NO_CONTEXT, currentContext)
+            // Subsequent calls to initialize should be ignored
+            // and the current EglContext should be the same as the previous call
+            initialize()
+            assertTrue(currentContext === eglContext)
+            assertTrue(currentConfig === eglConfig)
+        }
+    }
+
+    @Test
+    fun testMultipleReleaseCallsIgnored() {
+        testEglManager {
+            initialize()
+            loadConfig(EglConfigAttributes8888)?.also {
+                createContext(it)
+            }
+            // Multiple attempts to release should act as no-ops, i.e. we should not crash
+            // and the corresponding context should be nulled out
+            release()
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+
+            release()
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+        }
+    }
+
+    @Test
+    fun testDefaultSurface() {
+        testEglManager {
+            initialize()
+
+            assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+            val config = loadConfig(EglConfigAttributes8888)
+
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+
+            createContext(config!!)
+
+            if (isExtensionSupported(EglKhrSurfacelessContext)) {
+                assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            } else {
+                assertNotEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            }
+
+            assertEquals(currentDrawSurface, defaultSurface)
+            assertEquals(currentReadSurface, defaultSurface)
+
+            release()
+
+            assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+        }
+    }
+
+    @Test
+    fun testDefaultSurfaceWithoutSurfacelessContext() {
+        // Create a new EGL Spec instance that does not support the
+        // EglKhrSurfacelessContext extension in order to verify
+        // the fallback support of initializing the current surface
+        // to a PBuffer instead of EGL14.EGL_NO_SURFACE
+        val wrappedEglSpec = object : EglSpec by EglSpec.Egl14 {
+            override fun eglQueryString(nameId: Int): String {
+                val queryString = EglSpec.Egl14.eglQueryString(nameId)
+                return if (nameId == EGL14.EGL_EXTENSIONS) {
+                    // Parse the space separated string of EGL extensions into a set
+                    val set = HashSet<String>().apply {
+                        addAll(queryString.split(' '))
+                    }
+                    // Remove EglKhrSurfacelessContext if it exists
+                    // and repack the set into a space separated string
+                    set.remove(EglKhrSurfacelessContext)
+                    StringBuilder().let {
+                        for (entry in set) {
+                            it.append(entry)
+                            it.append(' ')
+                        }
+                        it.toString()
+                    }
+                } else {
+                    queryString
+                }
+            }
+        }
+
+        testEglManager(wrappedEglSpec) {
+            initialize()
+
+            // Verify that the wrapped EGL spec implementation in fact does not
+            // advertise support for EglKhrSurfacelessContext
+            assertFalse(isExtensionSupported(EglKhrSurfacelessContext))
+
+            assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+            val config = loadConfig(EglConfigAttributes8888)
+
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+
+            // Create context at this point should fallback of eglCreatePBufferSurface
+            // instead of EGL_NO_SURFACE as a result of no longer advertising support
+            // for EglKhrSurfacelessContext
+            createContext(config!!)
+
+            assertNotEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, defaultSurface)
+            assertEquals(currentReadSurface, defaultSurface)
+
+            release()
+
+            assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+        }
+    }
+
+    @Test
+    fun testCreatePBufferSurface() {
+        testEglManager {
+            initialize()
+
+            assertEquals(defaultSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentDrawSurface, EGL14.EGL_NO_SURFACE)
+            assertEquals(currentReadSurface, EGL14.EGL_NO_SURFACE)
+
+            val config = loadConfig(EglConfigAttributes8888)
+
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+            createContext(config!!)
+
+            val pBuffer = eglSpec.eglCreatePBufferSurface(
+                config,
+                EglConfigAttributes {
+                    EGL14.EGL_WIDTH to 1
+                    EGL14.EGL_HEIGHT to 1
+                })
+
+            makeCurrent(pBuffer)
+
+            assertNotEquals(EGL14.EGL_NO_SURFACE, currentReadSurface)
+            assertNotEquals(EGL14.EGL_NO_SURFACE, currentDrawSurface)
+            assertNotEquals(EGL14.EGL_NO_SURFACE, pBuffer)
+
+            assertEquals(pBuffer, currentReadSurface)
+            assertEquals(pBuffer, currentDrawSurface)
+
+            eglSpec.eglDestroySurface(pBuffer)
+            release()
+        }
+    }
+
+    @Test
+    fun testCreateWindowSurfaceDefault() {
+        testEglManager {
+            initialize()
+
+            val config = loadConfig(EglConfigAttributes8888)
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+
+            createContext(config!!)
+
+            val surface = Surface(SurfaceTexture(42))
+            // Create a window surface with the default attributes
+            val eglSurface = eglSpec.eglCreateWindowSurface(config, surface, null)
+            assertNotEquals(EGL14.EGL_NO_SURFACE, eglSurface)
+            eglSpec.eglDestroySurface(eglSurface)
+
+            release()
+        }
+    }
+
+    private fun EglSpec.isSingleBufferedSurface(surface: EGLSurface): Boolean {
+        return if (surface == EGL14.EGL_NO_SURFACE) {
+            false
+        } else {
+            val result = IntArray(1)
+            val queryResult = eglQuerySurface(
+                surface, EGL14.EGL_RENDER_BUFFER, result, 0)
+            queryResult && result[0] == EGL14.EGL_SINGLE_BUFFER
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    @Test
+    fun testSurfaceContentsWithBackBuffer() {
+        verifySurfaceContentsWithWindowConfig()
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    @Test
+    fun testSurfaceContentsWithFrontBuffer() {
+        verifySurfaceContentsWithWindowConfig(true)
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    private fun verifySurfaceContentsWithWindowConfig(
+        singleBuffered: Boolean = false
+    ) {
+        testEglManager {
+            initialize()
+            val config = loadConfig(EglConfigAttributes8888)
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+            createContext(config!!)
+
+            val width = 8
+            val height = 5
+            val targetColor = Color.RED
+            val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
+            var canRender = false
+
+            thread {
+                canRender = drawSurface(imageReader.surface, targetColor, singleBuffered)
+            }.join()
+
+            try {
+                if (canRender) {
+                    val image = imageReader.acquireLatestImage()
+                    val plane = image.planes[0]
+                    assertEquals(4, plane.pixelStride)
+
+                    val pixelStride = plane.pixelStride
+                    val rowStride = plane.rowStride
+                    val rowPadding = rowStride - pixelStride * width
+                    var offset = 0
+                    for (y in 0 until height) {
+                        for (x in 0 until width) {
+                            val red = plane.buffer[offset].toInt() and 0xff
+                            val green = plane.buffer[offset + 1].toInt() and 0xff
+                            val blue = plane.buffer[offset + 2].toInt() and 0xff
+                            val alpha = plane.buffer[offset + 3].toInt() and 0xff
+                            val packedColor = Color.argb(alpha, red, green, blue)
+                            assertEquals("Index: " + x + ", " + y, targetColor, packedColor)
+                            offset += pixelStride
+                        }
+                        offset += rowPadding
+                    }
+                }
+            } finally {
+                imageReader.close()
+                release()
+            }
+        }
+    }
+
+    private fun drawSurface(
+        surface: Surface,
+        color: Int,
+        singleBuffered: Boolean
+    ): Boolean {
+        var canRender = false
+        testEglManager {
+            initialize()
+            val config = loadConfig(EglConfigAttributes8888)
+            if (config == null) {
+                fail("Config 8888 should be supported")
+            }
+            createContext(config!!)
+            val configAttributes = if (singleBuffered) {
+                EglConfigAttributes {
+                    EGL14.EGL_RENDER_BUFFER to EGL14.EGL_SINGLE_BUFFER
+                }
+            } else {
+                null
+            }
+            val eglSurface = eglSpec.eglCreateWindowSurface(config, surface, configAttributes)
+            // Skip tests of the device does not support EGL_SINGLE_BUFFER
+            canRender = !singleBuffered || eglSpec.isSingleBufferedSurface(eglSurface)
+            if (canRender) {
+                makeCurrent(eglSurface)
+                assertEquals("Make current failed", EGL14.EGL_SUCCESS, eglSpec.eglGetError())
+                GLES20.glClearColor(
+                    Color.red(color) / 255f,
+                    Color.green(color) / 255f,
+                    Color.blue(color) / 255f,
+                    Color.alpha(color) / 255f
+                )
+                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                swapAndFlushBuffers()
+                assertEquals("Swapbuffers failed", EGL14.EGL_SUCCESS, eglSpec.eglGetError())
+            }
+
+            eglSpec.eglDestroySurface(eglSurface)
+            release()
+        }
+        return canRender
+    }
+
+    /**
+     * Helper method to ensure EglManager has the corresponding release calls
+     * made to it and verifies that no exceptions were thrown as part of the test.
+     */
+    private fun testEglManager(
+        eglSpec: EglSpec = EglSpec.Egl14,
+        block: EglManager.() -> Unit = {}
+    ) {
+        with(EglManager(eglSpec)) {
+            assertEquals(EglVersion.Unknown, eglVersion)
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+            block()
+            release()
+            assertEquals(EglVersion.Unknown, eglVersion)
+            assertEquals(EGL14.EGL_NO_CONTEXT, eglContext)
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt
new file mode 100644
index 0000000..35c4355
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglTestActivity.kt
@@ -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.graphics.opengl.egl
+
+import android.animation.ValueAnimator
+import android.app.Activity
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.os.Bundle
+import android.view.Surface
+import android.view.SurfaceView
+import android.view.TextureView
+import android.widget.LinearLayout
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.GLRenderer.RenderTarget
+import java.util.concurrent.atomic.AtomicInteger
+
+const val TAG: String = "EGLTestActivity"
+
+class EglTestActivity : Activity() {
+
+    private val mGLRenderer = GLRenderer()
+    private val mParam = AtomicInteger()
+    private val mRenderer1 = object : GLRenderer.RenderCallback {
+        override fun onSurfaceCreated(
+            spec: EglSpec,
+            config: EGLConfig,
+            surface: Surface,
+            width: Int,
+            height: Int
+        ): EGLSurface {
+            val attrs = EglConfigAttributes {
+                EGL14.EGL_RENDER_BUFFER to EGL14.EGL_SINGLE_BUFFER
+            }
+            return spec.eglCreateWindowSurface(config, surface, attrs)
+        }
+
+        override fun onDrawFrame(eglManager: EglManager) {
+            val red = mParam.toFloat() / 100f
+            GLES20.glClearColor(red, 0.0f, 0.0f, 1.0f)
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        }
+    }
+
+    private val mRenderer2 = object : GLRenderer.RenderCallback {
+        override fun onDrawFrame(eglManager: EglManager) {
+            val blue = mParam.toFloat() / 100f
+            GLES20.glClearColor(0.0f, 0.0f, blue, 1.0f)
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        }
+    }
+
+    private lateinit var mSurfaceView: SurfaceView
+    private lateinit var mTextureView: TextureView
+    private lateinit var mRenderTarget1: RenderTarget
+    private lateinit var mRenderTarget2: RenderTarget
+
+    private var mAnimator: ValueAnimator? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mGLRenderer.start()
+
+        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+            duration = 3000
+            repeatCount = ValueAnimator.INFINITE
+            repeatMode = ValueAnimator.REVERSE
+            addUpdateListener {
+                mParam.set(((it.animatedValue as Float) * 100).toInt())
+                mRenderTarget1.requestRender()
+                mRenderTarget2.requestRender()
+            }
+            start()
+        }
+
+        val container = LinearLayout(this).apply {
+            orientation = LinearLayout.VERTICAL
+            weightSum = 2f
+        }
+        mSurfaceView = SurfaceView(this)
+        mTextureView = TextureView(this)
+
+        val params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0).apply {
+            weight = 1f
+        }
+
+        mRenderTarget1 = mGLRenderer.attach(mSurfaceView, mRenderer1)
+        mRenderTarget2 = mGLRenderer.attach(mTextureView, mRenderer2)
+
+        container.addView(mSurfaceView, params)
+        container.addView(mTextureView, params)
+
+        setContentView(container)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        if (!mRenderTarget1.isAttached()) {
+            mRenderTarget1 = mGLRenderer.attach(mSurfaceView, mRenderer1)
+        }
+
+        if (!mRenderTarget2.isAttached()) {
+            mRenderTarget2 = mGLRenderer.attach(mTextureView, mRenderer2)
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        mRenderTarget1.detach(true)
+        mRenderTarget2.detach(true)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mAnimator?.cancel()
+        mGLRenderer.stop(true)
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt
new file mode 100644
index 0000000..87e2208
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EglVersionTest.kt
@@ -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.graphics.opengl.egl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EglVersionTest {
+
+    @Test
+    fun testDestructuringComponents() {
+        val (major, minor) = EglVersion(8, 3)
+        assertEquals(8, major)
+        assertEquals(3, minor)
+    }
+
+    @Test
+    fun testEquals() {
+        assertEquals(EglVersion(2, 9), EglVersion(2, 9))
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("EGL version 5.9", EglVersion(5, 9).toString())
+    }
+
+    @Test
+    fun testHashCode() {
+        val hashCode = 31 * 8 + 4
+        assertEquals(hashCode, EglVersion(8, 4).hashCode())
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
new file mode 100644
index 0000000..ccb26b8
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -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.graphics.surface
+
+import android.os.Build
+import android.view.Surface
+import android.view.SurfaceControl
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RequiresApi(Build.VERSION_CODES.Q)
+@SdkSuppress(minSdkVersion = 29)
+class SurfaceControlCompatTest {
+
+    @Test
+    fun testCreateFromWindow() {
+        var surfaceControl = SurfaceControl.Builder()
+            .setName("SurfaceControlCompact_createFromWindow")
+            .build()
+        try {
+            SurfaceControlCompat(Surface(surfaceControl), "SurfaceControlCompatTest")
+        } catch (e: IllegalArgumentException) {
+            fail()
+        }
+    }
+
+    @Test
+    fun testSurfaceTransactionCreate() {
+        try {
+            SurfaceControlCompat.Transaction()
+        } catch (e: java.lang.IllegalArgumentException) {
+            fail()
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/AndroidManifest.xml b/graphics/graphics-core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0082b69
--- /dev/null
+++ b/graphics/graphics-core/src/main/AndroidManifest.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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.graphics.core">
+
+</manifest>
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md b/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md
new file mode 100644
index 0000000..ffaf9e0
--- /dev/null
+++ b/graphics/graphics-core/src/main/androidx/graphics/androidx-graphics-graphics-core-documentation.md
@@ -0,0 +1,5 @@
+# Module root
+
+AndroidX Graphics Core
+
+# Support classes for building applications that leverage more advanced graphics facilities
diff --git a/graphics/graphics-core/src/main/cpp/CMakeLists.txt b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..6349ccf
--- /dev/null
+++ b/graphics/graphics-core/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,53 @@
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.18.1)
+
+# Declares and names the project.
+
+project("graphics-core")
+
+add_definitions(-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+add_library( # Sets the name of the library.
+             graphics-core
+
+             # Sets the library as a shared library.
+             SHARED
+
+             # Provides a relative path to your source file(s).
+             graphics-core.cpp )
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+              log-lib
+
+              # Specifies the name of the NDK library that
+              # you want CMake to locate.
+              log )
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+                       graphics-core
+
+                       # Links the target library to the log library
+                       # included in the NDK.
+                       ${log-lib}
+
+                       android)
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
new file mode 100644
index 0000000..6255927
--- /dev/null
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+#include <jni.h>
+#include <string>
+#include <android/native_activity.h>
+#include <android/surface_control.h>
+#include <android/api-level.h>
+#include <android/native_window_jni.h>
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_nCreateFromWindow(JNIEnv *env, jobject thiz,
+                                                                      jobject surface,
+                                                                      jstring debug_name) {
+    if (android_get_device_api_level() >= 29) {
+        auto AWindow = ANativeWindow_fromSurface(env, surface);
+        auto debugName = env->GetStringUTFChars(debug_name, nullptr);
+        auto surfaceControl = reinterpret_cast<jlong>(ASurfaceControl_createFromWindow(AWindow,
+                                                                                       debugName));
+
+        ANativeWindow_release(AWindow);
+        return surfaceControl;
+    } else {
+        return 0;
+    }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_nRelease(JNIEnv *env, jobject thiz,
+                                                             jlong surfaceControl) {
+    if (android_get_device_api_level() >= 29) {
+        ASurfaceControl_release(reinterpret_cast<ASurfaceControl *>(surfaceControl));
+    } else {
+        return;
+    }
+}
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_00024Transaction_nTransactionCreate(
+        JNIEnv *env, jobject thiz) {
+    if (android_get_device_api_level() >= 29) {
+        return reinterpret_cast<jlong>(ASurfaceTransaction_create());
+    } else {
+        return 0;
+    }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_androidx_graphics_surface_SurfaceControlCompat_00024Transaction_nTransactionDelete(
+        JNIEnv *env, jobject thiz,
+        jlong surfaceTransaction) {
+    if (android_get_device_api_level() >= 29) {
+        ASurfaceTransaction_delete(reinterpret_cast<ASurfaceTransaction *>(surfaceTransaction));
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
new file mode 100644
index 0000000..f150a79
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -0,0 +1,681 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.graphics.opengl
+
+import android.graphics.SurfaceTexture
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.egl.EglConfigAttributes8888
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Class responsible for coordination of requests to render into surfaces using OpenGL.
+ * This creates a backing thread to handle EGL dependencies and draw leveraging OpenGL across
+ * multiple [android.view.Surface] instances that can be attached and detached throughout
+ * the lifecycle of an application. Usage of this class is recommended to be done on the UI thread.
+ *
+ * @param eglSpecFactory Callback invoked to determine the EGL spec version to use
+ * for EGL management. This is invoked on the GL Thread
+ * @param eglConfigFactory Callback invoked to determine the appropriate EGLConfig used
+ * to create the EGL context. This is invoked on the GL Thread
+ */
+// GL is the industry standard for referencing OpenGL vs Gl (lowercase l)
+@Suppress("AcronymName")
+class GLRenderer(
+    eglSpecFactory: () -> EglSpec = { EglSpec.Egl14 },
+    eglConfigFactory: EglManager.() -> EGLConfig = {
+        // 8 bit channels should always be supported
+        loadConfig(EglConfigAttributes8888)
+            ?: throw IllegalStateException("Unable to obtain config for 8 bit EGL " +
+                "configuration")
+    }
+) {
+
+    /**
+     * Factory method to determine which EglSpec the underlying EglManager implementation uses
+     */
+    private val mEglSpecFactory: () -> EglSpec = eglSpecFactory
+
+    /**
+     * Factory method used to create the corresponding EGLConfig used to create the EGLRenderer used
+     * by EglManager
+     */
+    private val mEglConfigFactory: EglManager.() -> EGLConfig = eglConfigFactory
+
+    /**
+     * GLThread used to manage EGL dependencies, create EGLSurfaces and draw content
+     */
+    private var mGLThread: GLThread? = null
+
+    /**
+     * Collection of [RenderTarget] instances that are managed by the GLRenderer
+     */
+    private val mRenderTargets = ArrayList<RenderTarget>()
+
+    /**
+     * Collection of callbacks to be invoked when the EGL dependencies are initialized
+     * or torn down
+     */
+    private val mEglContextCallback = HashSet<EglContextCallback>()
+
+    /**
+     * Removes the corresponding [RenderTarget] from management of the GLThread.
+     * This destroys the EGLSurface associated with this surface and subsequent requests
+     * to render into the surface with the provided token are ignored.
+     *
+     * If the [cancelPending] flag is set to true, any queued request
+     * to render that has not started yet is cancelled. However, if this is invoked in the
+     * middle of the frame being rendered, it will continue to process the current frame.
+     *
+     * Additionally if this flag is false, all pending requests to render will be processed
+     * before the [RenderTarget] is detached.
+     *
+     * Note the detach operation will only occur if the GLRenderer is started, that is if
+     * [isRunning] returns true. Otherwise this is a no-op. GLRenderer will automatically detach all
+     * [RenderTarget] instances as part of its teardown process.
+     */
+    @JvmOverloads
+    fun detach(
+        target: RenderTarget,
+        cancelPending: Boolean,
+        @WorkerThread onDetachComplete: ((RenderTarget) -> Unit)? = null
+    ) {
+        if (mRenderTargets.contains(target)) {
+            mGLThread?.detachSurface(target.token, cancelPending) {
+                // WorkerThread
+                target.release()
+                target.onDetach.invoke()
+                onDetachComplete?.invoke(target)
+            }
+            mRenderTargets.remove(target)
+        }
+    }
+
+    /**
+     * Determines if the GLThread has been started. That is [start] has been invoked
+     * on this GLRenderer instance without a corresponding call to [stop].
+     */
+    fun isRunning(): Boolean = mGLThread != null
+
+    /**
+     * Starts the GLThread. After this method is called, consumers can attempt
+     * to attach [android.view.Surface] instances through [attach] as well as
+     * schedule content to be drawn through [requestRender]
+     *
+     * @param name Optional name to provide to the GLThread
+     *
+     * @throws IllegalStateException if EGLConfig with desired attributes cannot be created
+     */
+    @JvmOverloads
+    fun start(
+        name: String = "GLThread",
+    ) {
+        if (mGLThread == null) {
+            GLThread.log("starting thread...")
+            mGLThread = GLThread(
+                name,
+                mEglSpecFactory,
+                mEglConfigFactory
+            ).apply {
+                start()
+                if (!mEglContextCallback.isEmpty()) {
+                    // Add a copy of the current collection as new entries to mEglContextCallback
+                    // could be mistakenly added multiple times.
+                    this.addEglCallbacks(ArrayList<EglContextCallback>(mEglContextCallback))
+                }
+            }
+        }
+    }
+
+    /**
+     * Mark the corresponding surface session with the given token as dirty
+     * to schedule a call to [RenderCallback#onDrawFrame].
+     * If there is already a queued request to render into the provided surface with
+     * the specified token, this request is ignored.
+     *
+     * Note the render operation will only occur if the GLRenderer is started, that is if
+     * [isRunning] returns true. Otherwise this is a no-op.
+     *
+     * @param target RenderTarget to be re-rendered
+     * @param onRenderComplete Optional callback invoked on the backing thread after the frame has
+     * been rendered.
+     */
+    @JvmOverloads
+    fun requestRender(target: RenderTarget, onRenderComplete: ((RenderTarget) -> Unit)? = null) {
+        val token = target.token
+        val callbackRunnable = if (onRenderComplete != null) {
+            Runnable {
+                onRenderComplete.invoke(target)
+            }
+        } else {
+            null
+        }
+        mGLThread?.requestRender(token, callbackRunnable)
+    }
+
+    /**
+     * Resize the corresponding surface associated with the RenderTarget to the specified
+     * width and height and re-render. This will destroy the EGLSurface created by
+     * [RenderCallback.onSurfaceCreated] and invoke it again with the updated dimensions.
+     * An optional callback is invoked on the backing thread after the resize operation
+     * is complete.
+     *
+     * Note the resize operation will only occur if the GLRenderer is started, that is if
+     * [isRunning] returns true. Otherwise this is a no-op.
+     *
+     * @param target RenderTarget to be resized
+     * @param width Updated width of the corresponding surface
+     * @param height Updated height of the corresponding surface
+     * @param onResizeComplete Optional callback invoked on the backing thread when the resize
+     * operation is complete
+     */
+    @JvmOverloads
+    fun resize(
+        target: RenderTarget,
+        width: Int,
+        height: Int,
+        onResizeComplete: ((RenderTarget) -> Unit)? = null
+    ) {
+        val token = target.token
+        val callbackRunnable = if (onResizeComplete != null) {
+            Runnable {
+                onResizeComplete.invoke(target)
+            }
+        } else {
+            null
+        }
+        mGLThread?.resizeSurface(token, width, height, callbackRunnable)
+    }
+
+    /**
+     * Stop the corresponding GL thread. This destroys all EGLSurfaces as well
+     * as any other EGL dependencies. All queued requests that have not been processed
+     * yet are cancelled.
+     *
+     * Note the stop operation will only occur if the GLRenderer was previously started, that is
+     * [isRunning] returns true. Otherwise this is a no-op.
+     *
+     * @param cancelPending If true all pending requests and cancelled and the backing thread is
+     * torn down immediately. If false, all pending requests are processed first before tearing
+     * down the backing thread. Subsequent requests made after this call are ignored.
+     * @param onStop Optional callback invoked on the backing thread after it is torn down.
+     */
+    @JvmOverloads
+    fun stop(cancelPending: Boolean, onStop: ((GLRenderer) -> Unit)? = null) {
+        GLThread.log("stopping thread...")
+        // Make a copy of the render targets to call cleanup operations on to avoid potential
+        // concurrency issues.
+        // This method will clear the existing collection and we do not want to potentially tear
+        // down a target that was attached after a subsequent call to start if the tear down
+        // callback execution is delayed if previously pending requests have not been cancelled
+        // (i.e. cancelPending is false)
+        val renderTargets = ArrayList(mRenderTargets)
+        mGLThread?.tearDown(cancelPending) {
+            // No need to call target.detach as this callback is invoked after
+            // the dependencies are cleaned up
+            for (target in renderTargets) {
+                target.release()
+                target.onDetach.invoke()
+            }
+            onStop?.invoke(this@GLRenderer)
+        }
+        mGLThread = null
+        mRenderTargets.clear()
+    }
+
+    /**
+     * Add an [EglContextCallback] to receive callbacks for construction and
+     * destruction of EGL dependencies.
+     *
+     * These callbacks are invoked on the backing thread.
+     */
+    fun registerEglContextCallback(callback: EglContextCallback) {
+        mEglContextCallback.add(callback)
+        mGLThread?.addEglCallback(callback)
+    }
+
+    /**
+     * Remove [EglContextCallback] to no longer receive callbacks for construction and
+     * destruction of EGL dependencies.
+     *
+     * These callbacks are invoked on the backing thread
+     */
+    fun unregisterEglContextCallback(callback: EglContextCallback) {
+        mEglContextCallback.remove(callback)
+        mGLThread?.removeEglCallback(callback)
+    }
+
+    /**
+     * Callbacks invoked when the GL dependencies are created and destroyed.
+     * These are logical places to setup and tear down any dependencies that are used
+     * for drawing content within a frame (ex. compiling shaders)
+     */
+    interface EglContextCallback {
+
+        /**
+         * Callback invoked on the backing thread after EGL dependencies are initialized.
+         * This is guaranteed to be invoked before any instance of
+         * [RenderCallback.onSurfaceCreated] is called.
+         * This will be invoked lazily before the first request to [GLRenderer.requestRender]
+         */
+        @WorkerThread
+        fun onEglContextCreated(eglManager: EglManager)
+
+        /**
+         * Callback invoked on the backing thread before EGL dependencies are about to be torn down.
+         * This is invoked after [GLRenderer.stop] is processed.
+         */
+        @WorkerThread
+        fun onEglContextDestroyed(eglManager: EglManager)
+    }
+
+    /**
+     * Interface used for creating an [EGLSurface] with a user defined configuration
+     * from the provided surface as well as a callback used to render content into the surface
+     * for a given frame
+     */
+    interface RenderCallback {
+        /**
+         * Used to create a corresponding [EGLSurface] from the provided
+         * [android.view.Surface] instance. This enables consumers to configure
+         * the corresponding [EGLSurface] they wish to render into.
+         * The [EGLSurface] created here is guaranteed to be the current surface
+         * before [onDrawFrame] is called. That is, implementations of onDrawFrame
+         * do not need to call eglMakeCurrent on this [EGLSurface].
+         *
+         * This method is invoked on the GL thread.
+         *
+         * The default implementation will create a window surface with EGL_WIDTH and EGL_HEIGHT
+         * set to [width] and [height] respectively.
+         * Implementations can override this method to provide additional EglConfigAttributes
+         * for this surface (ex. [EGL14.EGL_SINGLE_BUFFER]
+         *
+         * @param spec EGLSpec used to create the corresponding EGLSurface
+         * @param config EGLConfig used to create the corresponding EGLSurface
+         * @param surface [android.view.Surface] used to create an EGLSurface from
+         * @param width Desired width of the surface to create
+         * @param height Desired height of the surface to create
+         */
+        @WorkerThread
+        fun onSurfaceCreated(
+            spec: EglSpec,
+            config: EGLConfig,
+            surface: Surface,
+            width: Int,
+            height: Int
+        ): EGLSurface =
+            // Always default to creating an EGL window surface
+            // Despite having access to the width and height here, do not explicitly
+            // pass in EGLConfigAttributes specifying the EGL_WIDTH and EGL_HEIGHT parameters
+            // as those are not accepted parameters for eglCreateWindowSurface but they are
+            // for other EGL Surface factory methods such as eglCreatePBufferSurface
+            // See accepted parameters here:
+            // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
+            // and here
+            // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
+            spec.eglCreateWindowSurface(config, surface, null)
+
+        /**
+         * Callback used to issue OpenGL drawing commands into the [EGLSurface]
+         * created in [onSurfaceCreated]. This [EGLSurface] is guaranteed to
+         * be current before this callback is invoked and [EglManager.swapAndFlushBuffers]
+         * will be invoked afterwards. If additional scratch [EGLSurface]s are used
+         * here it is up to the implementation of this method to ensure that the proper
+         * surfaces are made current and the appropriate swap buffers call is made
+         *
+         * This method is invoked on the backing thread
+         *
+         * @param eglManager Handle to EGL dependencies
+         */
+        @WorkerThread
+        fun onDrawFrame(eglManager: EglManager)
+    }
+
+    /**
+     * Adds the [android.view.Surface] to be managed by the GLThread.
+     * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+     * for rendering into the surface through [RenderCallback].
+     * Unlike the other [attach] methods that consume a [SurfaceView] or [TextureView],
+     * this method does not handle any lifecycle callbacks associated with the target surface.
+     * Therefore it is up to the consumer to properly setup/teardown resources associated with
+     * this surface.
+     *
+     * @param surface Target surface to be managed by the backing thread
+     * @param width Desired width of the [surface]
+     * @param height Desired height of the [surface]
+     * @param renderer Callbacks used to create a corresponding [EGLSurface] from the
+     * given surface as well as render content into the created [EGLSurface]
+     * @return [RenderTarget] used for subsequent requests to communicate
+     * with the provided Surface (ex. [requestRender] or [detach]).
+     *
+     * @throws IllegalStateException If this method was called when the GLThread has not started
+     * (i.e. start has not been called)
+     */
+    fun attach(surface: Surface, width: Int, height: Int, renderer: RenderCallback): RenderTarget {
+        val thread = mGLThread
+        if (thread != null) {
+            val token = sToken.getAndIncrement()
+            thread.attachSurface(token, surface, width, height, renderer)
+            return RenderTarget(token, this).also { mRenderTargets.add(it) }
+        } else {
+            throw IllegalStateException("GLThread not started, did you forget to call start?")
+        }
+    }
+
+    /**
+     * Adds the [android.view.Surface] provided by the given [SurfaceView] to be managed by the
+     * backing thread.
+     *
+     * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+     * for rendering into the surface through [RenderCallback].
+     *
+     * This method automatically configures a [SurfaceHolder.Callback] used to attach the
+     * [android.view.Surface] when the underlying [SurfaceHolder] that contains the surface is
+     * available. Similarly this surface will be detached from [GLRenderer] when the surface provided
+     * by the [SurfaceView] is destroyed (i.e. [SurfaceHolder.Callback.surfaceDestroyed] is called.
+     *
+     * If the [android.view.Surface] is already available by the time this method is invoked,
+     * it is attached synchronously.
+     *
+     * @param surfaceView SurfaceView that provides the surface to be rendered by the backing thread
+     * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+     * given surface as well as render content into the created [EGLSurface]
+     * @return [RenderTarget] used for subsequent requests to communicate
+     * with the provided Surface (ex. [requestRender] or [detach]).
+     *
+     * @throws IllegalStateException If this method was called when the GLThread has not started
+     * (i.e. start has not been called)
+     */
+    fun attach(surfaceView: SurfaceView, renderer: RenderCallback): RenderTarget {
+        val thread = mGLThread
+        if (thread != null) {
+            val token = sToken.getAndIncrement()
+            val holder = surfaceView.holder
+            val callback = object : SurfaceHolder.Callback2 {
+
+                var isAttached = false
+
+                /**
+                 * Optional condition that maybe used if we are issuing a blocking call to render
+                 * in [SurfaceHolder.Callback2.surfaceRedrawNeeded]
+                 * In this case we need to signal the condition of either the request to render
+                 * has completed, or if the RenderTarget has been detached and the pending
+                 * render request is cancelled.
+                 */
+                @Volatile var renderLatch: CountDownLatch? = null
+
+                val renderTarget = RenderTarget(token, this@GLRenderer) @WorkerThread {
+                    isAttached = false
+                    // SurfaceHolder.add/remove callback is thread safe
+                    holder.removeCallback(this)
+                    // Countdown in case we have been detached while waiting for a render
+                    // to be completed
+                    renderLatch?.countDown()
+                }
+
+                override fun surfaceRedrawNeeded(p0: SurfaceHolder) {
+                    val latch = CountDownLatch(1).also { renderLatch = it }
+                    // Request a render and block until the rendering is complete
+                    // surfaceRedrawNeeded is invoked on older API levels and is replaced with
+                    // surfaceRedrawNeededAsync for newer API levels which is non-blocking
+                    renderTarget.requestRender @WorkerThread {
+                        latch.countDown()
+                    }
+                    latch.await()
+                    renderLatch = null
+                }
+
+                override fun surfaceRedrawNeededAsync(
+                    holder: SurfaceHolder,
+                    drawingFinished: Runnable
+                ) {
+                    renderTarget.requestRender {
+                        drawingFinished.run()
+                    }
+                }
+
+                override fun surfaceCreated(holder: SurfaceHolder) {
+                    // NO-OP wait until surfaceChanged which is guaranteed to be called and also
+                    // provides the appropriate width height of the surface
+                }
+
+                override fun surfaceChanged(
+                    holder: SurfaceHolder,
+                    format: Int,
+                    width: Int,
+                    height: Int
+                ) {
+                    if (!isAttached) {
+                        thread.attachSurface(token, holder.surface, width, height, renderer)
+                        isAttached = true
+                    } else {
+                        renderTarget.resize(width, height)
+                    }
+                    renderTarget.requestRender()
+                }
+
+                override fun surfaceDestroyed(holder: SurfaceHolder) {
+                    val detachLatch = CountDownLatch(1)
+                    renderTarget.detach(true) {
+                        detachLatch.countDown()
+                    }
+                    detachLatch.await()
+                }
+            }
+            holder.addCallback(callback)
+            if (holder.surface != null && holder.surface.isValid) {
+                thread.attachSurface(
+                    token,
+                    holder.surface,
+                    surfaceView.width,
+                    surfaceView.height,
+                    renderer
+                )
+            }
+            mRenderTargets.add(callback.renderTarget)
+            return callback.renderTarget
+        } else {
+            throw IllegalStateException("GLThread not started, did you forget to call start?")
+        }
+    }
+
+    /**
+     * Adds the [android.view.Surface] provided by the given [TextureView] to be managed by the
+     * backing thread.
+     *
+     * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+     * for rendering into the surface through [RenderCallback].
+     *
+     * This method automatically configures a [TextureView.SurfaceTextureListener] used to create a
+     * [android.view.Surface] when the underlying [SurfaceTexture] is available.
+     * Similarly this surface will be detached from [GLRenderer] if the underlying [SurfaceTexture]
+     * is destroyed (i.e. [TextureView.SurfaceTextureListener.onSurfaceTextureDestroyed] is called.
+     *
+     * If the [SurfaceTexture] is already available by the time this method is called, then it is
+     * attached synchronously.
+     *
+     * @param textureView TextureView that provides the surface to be rendered into on the GLThread
+     * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+     * given surface as well as render content into the created [EGLSurface]
+     * @return [RenderTarget] used for subsequent requests to communicate
+     * with the provided Surface (ex. [requestRender] or [detach]).
+     *
+     * @throws IllegalStateException If this method was called when the GLThread has not started
+     * (i.e. start has not been called)
+     */
+    fun attach(textureView: TextureView, renderer: RenderCallback): RenderTarget {
+        val thread = mGLThread
+        if (thread != null) {
+            val token = sToken.getAndIncrement()
+            val renderTarget = RenderTarget(token, this) @WorkerThread {
+                textureView.handler?.post {
+                    textureView.surfaceTextureListener = null
+                }
+            }
+            textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+                override fun onSurfaceTextureAvailable(
+                    surfaceTexture: SurfaceTexture,
+                    width: Int,
+                    height: Int
+                ) {
+                    thread.attachSurface(token, Surface(surfaceTexture), width, height, renderer)
+                }
+
+                override fun onSurfaceTextureSizeChanged(
+                    texture: SurfaceTexture,
+                    width: Int,
+                    height: Int
+                ) {
+                    renderTarget.resize(width, height)
+                    renderTarget.requestRender()
+                }
+
+                override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean {
+                    val detachLatch = CountDownLatch(1)
+                    renderTarget.detach(true) {
+                        detachLatch.countDown()
+                    }
+                    detachLatch.await()
+                    return true
+                }
+
+                override fun onSurfaceTextureUpdated(p0: SurfaceTexture) {
+                    // NO-OP
+                }
+            }
+            if (textureView.isAvailable) {
+                thread.attachSurface(
+                    token,
+                    Surface(textureView.surfaceTexture),
+                    textureView.width,
+                    textureView.height,
+                    renderer
+                )
+            }
+            mRenderTargets.add(renderTarget)
+            return renderTarget
+        } else {
+            throw IllegalStateException("GLThread not started, did you forget to call start?")
+        }
+    }
+
+    /**
+     * Handle to a [android.view.Surface] that is given to [GLRenderer] to handle
+     * rendering.
+     */
+    class RenderTarget internal constructor(
+        internal val token: Int,
+        glManager: GLRenderer,
+        @WorkerThread internal val onDetach: () -> Unit = {}
+    ) {
+
+        @Volatile
+        private var mManager: GLRenderer? = glManager
+
+        internal fun release() {
+            mManager = null
+        }
+
+        /**
+         * Request that this [RenderTarget] should have its contents redrawn.
+         * This consumes an optional callback that is invoked on the backing thread when
+         * the rendering is completed.
+         *
+         * Note the render operation will only occur if the RenderTarget is attached, that is
+         * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+         * created this RenderTarget is stopped, this is a no-op.
+         *
+         * @param onRenderComplete Optional callback called on the backing thread when
+         * rendering is finished
+         */
+        @JvmOverloads
+        fun requestRender(@WorkerThread onRenderComplete: ((RenderTarget) -> Unit)? = null) {
+            mManager?.requestRender(this@RenderTarget, onRenderComplete)
+        }
+
+        /**
+         * Determines if the current RenderTarget is attached to GLRenderer.
+         * This is true until [detach] has been called. If the RenderTarget is no longer
+         * in an attached state (i.e. this returns false). Subsequent calls to [requestRender]
+         * will be ignored.
+         */
+        fun isAttached(): Boolean = mManager != null
+
+        /**
+         * Resize the RenderTarget to the specified width and height.
+         * This will destroy the EGLSurface created by [RenderCallback.onSurfaceCreated]
+         * and invoke it again with the updated dimensions.
+         * An optional callback is invoked on the backing thread after the resize operation
+         * is complete
+         *
+         * Note the resize operation will only occur if the RenderTarget is attached, that is
+         * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+         * created this RenderTarget is stopped, this is a no-op.
+         *
+         * @param width New target width to resize the RenderTarget
+         * @param height New target height to resize the RenderTarget
+         * @param onResizeComplete Optional callback invoked after the resize is complete
+         */
+        @JvmOverloads
+        fun resize(
+            width: Int,
+            height: Int,
+            @WorkerThread onResizeComplete: ((RenderTarget) -> Unit)? = null
+        ) {
+            mManager?.resize(this, width, height, onResizeComplete)
+        }
+
+        /**
+         * Removes the corresponding [RenderTarget] from management of the GLThread.
+         * This destroys the EGLSurface associated with this surface and subsequent requests
+         * to render into the surface with the provided token are ignored.
+         *
+         * If the [cancelPending] flag is set to true, any queued request
+         * to render that has not started yet is cancelled. However, if this is invoked in the
+         * middle of the frame being rendered, it will continue to process the current frame.
+         *
+         * Additionally if this flag is false, all pending requests to render will be processed
+         * before the [RenderTarget] is detached.
+         *
+         * This is a convenience method around [GLRenderer.detach]
+         *
+         * Note the detach operation will only occur if the RenderTarget is attached, that is
+         * [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
+         * created this RenderTarget is stopped, this is a no-op.
+         */
+        @JvmOverloads
+        fun detach(cancelPending: Boolean, onDetachComplete: ((RenderTarget) -> Unit)? = null) {
+            mManager?.detach(this, cancelPending, onDetachComplete)
+        }
+    }
+
+    companion object {
+        /**
+         * Counter used to issue unique identifiers for surfaces that are managed by GLRenderer
+         */
+        private val sToken = AtomicInteger()
+    }
+}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
new file mode 100644
index 0000000..368ecf0
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
@@ -0,0 +1,384 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.graphics.opengl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.util.Log
+import android.view.Surface
+import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
+import androidx.graphics.opengl.GLRenderer.EglContextCallback
+import androidx.graphics.opengl.GLRenderer.RenderCallback
+import androidx.graphics.opengl.egl.EglManager
+import androidx.graphics.opengl.egl.EglSpec
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Thread responsible for management of EGL dependencies, setup and teardown
+ * of EGLSurface instances as well as delivering callbacks to draw a frame
+ */
+internal class GLThread(
+    name: String = "GLThread",
+    private val mEglSpecFactory: () -> EglSpec,
+    private val mEglConfigFactory: EglManager.() -> EGLConfig,
+) : HandlerThread(name) {
+
+    // Accessed on internal HandlerThread
+    private val mIsTearingDown = AtomicBoolean(false)
+    private var mEglManager: EglManager? = null
+    private val mSurfaceSessions = HashMap<Int, SurfaceSession>()
+    private var mHandler: Handler? = null
+    private val mEglContextCallback = HashSet<EglContextCallback>()
+
+    override fun start() {
+        super.start()
+        mHandler = Handler(looper)
+    }
+
+    /**
+     * Adds the given [android.view.Surface] to be managed by the GLThread.
+     * A corresponding [EGLSurface] is created on the GLThread as well as a callback
+     * for rendering into the surface through [RenderCallback].
+     * @param surface intended to be be rendered into on the GLThread
+     * @param width Desired width of the [surface]
+     * @param height Desired height of the [surface]
+     * @param renderer callbacks used to create a corresponding [EGLSurface] from the
+     * given surface as well as render content into the created [EGLSurface]
+     * @return Identifier used for subsequent requests to communicate
+     * with the provided Surface (ex. [requestRender] or [detachSurface]
+     */
+    @AnyThread
+    fun attachSurface(
+        token: Int,
+        surface: Surface,
+        width: Int,
+        height: Int,
+        renderer: RenderCallback
+    ) {
+        withHandler {
+            post(token) {
+                attachSurfaceSessionInternal(
+                    SurfaceSession(token, surface, renderer).apply {
+                        this.width = width
+                        this.height = height
+                    }
+                )
+            }
+        }
+    }
+
+    @AnyThread
+    fun resizeSurface(token: Int, width: Int, height: Int, callback: Runnable? = null) {
+        withHandler {
+            post(token) {
+                resizeSurfaceSessionInternal(token, width, height)
+                requestRenderInternal(token)
+                callback?.run()
+            }
+        }
+    }
+
+    @AnyThread
+    fun addEglCallbacks(callbacks: ArrayList<EglContextCallback>) {
+        withHandler {
+            post {
+                mEglContextCallback.addAll(callbacks)
+                mEglManager?.let {
+                    for (callback in callbacks) {
+                        callback.onEglContextCreated(it)
+                    }
+                }
+            }
+        }
+    }
+
+    @AnyThread
+    fun addEglCallback(callbacks: EglContextCallback) {
+        withHandler {
+            post {
+                mEglContextCallback.add(callbacks)
+                // If EGL dependencies are already initialized, immediately invoke
+                // the added callback
+                mEglManager?.let {
+                    callbacks.onEglContextCreated(it)
+                }
+            }
+        }
+    }
+
+    @AnyThread
+    fun removeEglCallback(callbacks: EglContextCallback) {
+        withHandler {
+            post {
+                mEglContextCallback.remove(callbacks)
+            }
+        }
+    }
+
+    /**
+     * Removes the corresponding [android.view.Surface] from management of the GLThread.
+     * This destroys the EGLSurface associated with this surface and subsequent requests
+     * to render into the surface with the provided token are ignored. Any queued request
+     * to render to the corresponding [SurfaceSession] that has not started yet is cancelled.
+     * However, if this is invoked in the middle of the frame being rendered, it will continue to
+     * process the current frame.
+     */
+    @AnyThread
+    fun detachSurface(
+        token: Int,
+        cancelPending: Boolean,
+        callback: Runnable?
+    ) {
+        log("dispatching request to detach surface w/ token: $token")
+        withHandler {
+            if (cancelPending) {
+                removeCallbacksAndMessages(token)
+            }
+            post(token) {
+                detachSurfaceSessionInternal(token, callback)
+            }
+        }
+    }
+
+    /**
+     * Cancel all pending requests to all currently managed [SurfaceSession] instances,
+     * destroy all EGLSurfaces, teardown EGLManager and quit this thread
+     */
+    @AnyThread
+    fun tearDown(cancelPending: Boolean, callback: Runnable?) {
+        withHandler {
+            if (cancelPending) {
+                removeCallbacksAndMessages(null)
+            }
+            post {
+                releaseResourcesInternalAndQuit(callback)
+            }
+            mIsTearingDown.set(true)
+        }
+    }
+
+    /**
+     * Mark the corresponding surface session with the given token as dirty
+     * to schedule a call to [RenderCallback#onDrawFrame].
+     * If there is already a queued request to render into the provided surface with
+     * the specified token, this request is ignored.
+     */
+    @AnyThread
+    fun requestRender(token: Int, callback: Runnable? = null) {
+        log("dispatching request to render for token: $token")
+        withHandler {
+            post(token) {
+                requestRenderInternal(token)
+                callback?.run()
+            }
+        }
+    }
+
+    /**
+     * Lazily creates an [EglManager] instance from the given [mEglSpecFactory]
+     * used to determine the configuration. This result is cached across calls
+     * unless [tearDown] has been called.
+     */
+    @WorkerThread
+    fun obtainEglManager(): EglManager =
+        mEglManager ?: EglManager(mEglSpecFactory.invoke()).also {
+            it.initialize()
+            val config = mEglConfigFactory.invoke(it)
+            it.createContext(config)
+            for (callback in mEglContextCallback) {
+                callback.onEglContextCreated(it)
+            }
+            mEglManager = it
+        }
+
+    @WorkerThread
+    private fun disposeSurfaceSession(session: SurfaceSession) {
+        val eglSurface = session.eglSurface
+        if (eglSurface != null && eglSurface != EGL14.EGL_NO_SURFACE) {
+            obtainEglManager().eglSpec.eglDestroySurface(eglSurface)
+            session.eglSurface = null
+        }
+    }
+
+    /**
+     * Helper method to obtain the cached EGLSurface for the given [SurfaceSession],
+     * creating one if it does not previously exist
+     */
+    @WorkerThread
+    private fun obtainEglSurfaceForSession(session: SurfaceSession): EGLSurface =
+        session.eglSurface ?: createEglSurfaceForSession(session)
+
+    /**
+     * Helper method to create the corresponding EGLSurface from the [SurfaceSession] instance
+     * Consumers are expected to teardown the previously existing EGLSurface instance if it exists
+     */
+    @WorkerThread
+    private fun createEglSurfaceForSession(
+        session: SurfaceSession
+    ): EGLSurface {
+        with(obtainEglManager()) {
+            val eglSurface = session.surfaceRenderer.onSurfaceCreated(
+                eglSpec,
+                // Successful creation of EglManager ensures non null EGLConfig
+                eglConfig!!,
+                session.surface,
+                session.width,
+                session.height
+            )
+            session.eglSurface = eglSurface
+            return eglSurface
+        }
+    }
+
+    @WorkerThread
+    private fun releaseResourcesInternalAndQuit(callback: Runnable?) {
+        mEglManager?.let { eglManager ->
+            for (session in mSurfaceSessions) {
+                disposeSurfaceSession(session.value)
+            }
+            callback?.run()
+            mSurfaceSessions.clear()
+            for (eglCallback in mEglContextCallback) {
+                eglCallback.onEglContextDestroyed(eglManager)
+            }
+            mEglContextCallback.clear()
+            eglManager.release()
+            mEglManager = null
+            quit()
+        }
+    }
+
+    @WorkerThread
+    private fun requestRenderInternal(token: Int) {
+        log("requesting render for token: $token")
+        mSurfaceSessions[token]?.let { surfaceSession ->
+            val eglManager = obtainEglManager()
+            eglManager.makeCurrent(obtainEglSurfaceForSession(surfaceSession))
+            val width = surfaceSession.width
+            val height = surfaceSession.height
+            if (width > 0 && height > 0) {
+                surfaceSession.surfaceRenderer.onDrawFrame(eglManager)
+            }
+            eglManager.swapAndFlushBuffers()
+        }
+    }
+
+    @WorkerThread
+    private fun attachSurfaceSessionInternal(surfaceSession: SurfaceSession) {
+        mSurfaceSessions[surfaceSession.surfaceToken] = surfaceSession
+    }
+
+    @WorkerThread
+    private fun resizeSurfaceSessionInternal(
+        token: Int,
+        width: Int,
+        height: Int
+    ) {
+        mSurfaceSessions[token]?.let { surfaceSession ->
+            surfaceSession.apply {
+                this.width = width
+                this.height = height
+            }
+            disposeSurfaceSession(surfaceSession)
+            obtainEglSurfaceForSession(surfaceSession)
+        }
+    }
+
+    @WorkerThread
+    private fun detachSurfaceSessionInternal(token: Int, callback: Runnable?) {
+        val session = mSurfaceSessions.remove(token)
+        if (session != null) {
+            disposeSurfaceSession(session)
+        }
+        callback?.run()
+    }
+
+    /**
+     * Helper method that issues a callback on the handler instance for this thread
+     * ensuring proper nullability checks are handled.
+     * This assumes that that [GLRenderer.start] has been called before attempts
+     * to interact with the corresponding Handler are made with this method
+     */
+    private inline fun withHandler(block: Handler.() -> Unit) {
+        val handler = mHandler
+            ?: throw IllegalStateException("Did you forget to call GLThread.start()?")
+        if (!mIsTearingDown.get()) {
+            block(handler)
+        }
+    }
+
+    companion object {
+
+        private const val DEBUG = true
+        private const val TAG = "GLThread"
+        internal fun log(msg: String) {
+            if (DEBUG) {
+                Log.v(TAG, msg)
+            }
+        }
+    }
+
+    private class SurfaceSession(
+        /**
+         * Identifier used to lookup the mapping of this surface session.
+         * Consumers are expected to provide this identifier to operate on the corresponding
+         * surface to either request a frame be rendered or to remove this Surface
+         */
+        val surfaceToken: Int,
+
+        /**
+         * Target surface to render into
+         */
+        val surface: Surface,
+
+        /**
+         * Callback used to create an EGLSurface from the provided surface as well as
+         * render content to the surface
+         */
+        val surfaceRenderer: RenderCallback
+    ) {
+        /**
+         * Lazily created + cached [EGLSurface] after [RenderCallback.onSurfaceCreated]
+         * is invoked. This is  only modified on the backing thread
+         */
+        var eglSurface: EGLSurface? = null
+
+        /**
+         * Target width of the [surface]. This is only modified on the backing thread
+         */
+        var width: Int = 0
+
+        /**
+         * Target height of the [surface]. This is only modified on the backing thread
+         */
+        var height: Int = 0
+    }
+
+    /**
+     * Handler does not expose a post method that takes a token and a runnable.
+     * We need the token to be able to cancel pending requests so just call
+     * postAtTime with the default of SystemClock.uptimeMillis
+     */
+    private fun Handler.post(token: Any?, runnable: Runnable) {
+        postAtTime(runnable, token, SystemClock.uptimeMillis())
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt
new file mode 100644
index 0000000..883b2e8
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglConfigAttributes.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.graphics.opengl.egl
+
+import android.opengl.EGL14
+
+/**
+ * EGL configuration attribute used to expose EGLConfigs that support formats with floating point
+ * RGBA components. This attribute is exposed through the EGL_EXT_pixel_format_float EGL extension
+ *
+ * See: https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_pixel_format_float.txt
+ */
+const val EglColorComponentTypeExt = 0x3339
+
+/**
+ * EGL configuration attribute value that represents fixed point RGBA components
+ */
+const val EglColorComponentTypeFixedExt = 0x333A
+
+/**
+ * EGL configuration attribute value that represents floating point RGBA components
+ */
+const val EglColorComponentTypeFloatExt = 0x333B
+
+/**
+ * EGL Attributes to create an 8 bit EGL config for red, green, blue, and alpha channels as well
+ * as an 8 bit stencil size
+ */
+val EglConfigAttributes8888 = EglConfigAttributes {
+    EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+    EGL14.EGL_RED_SIZE to 8
+    EGL14.EGL_GREEN_SIZE to 8
+    EGL14.EGL_BLUE_SIZE to 8
+    EGL14.EGL_ALPHA_SIZE to 8
+    EGL14.EGL_DEPTH_SIZE to 0
+    EGL14.EGL_CONFIG_CAVEAT to EGL14.EGL_NONE
+    EGL14.EGL_STENCIL_SIZE to 0
+    EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * EGL Attributes to create a 10 bit EGL config for red, green, blue, channels and a
+ * 2 bit alpha channels as well as an 8 bit stencil size
+ */
+val EglConfigAttributes1010102 = EglConfigAttributes {
+    EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+    EGL14.EGL_RED_SIZE to 10
+    EGL14.EGL_GREEN_SIZE to 10
+    EGL14.EGL_BLUE_SIZE to 10
+    EGL14.EGL_ALPHA_SIZE to 2
+    EGL14.EGL_DEPTH_SIZE to 0
+    EGL14.EGL_STENCIL_SIZE to 0
+    EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * EGL Attributes to create a 16 bit floating point EGL config for red, green and blue channels
+ * along with a
+ */
+val EglConfigAttributesF16 = EglConfigAttributes {
+    EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+    EglColorComponentTypeExt to EglColorComponentTypeFloatExt
+    EGL14.EGL_RED_SIZE to 16
+    EGL14.EGL_GREEN_SIZE to 16
+    EGL14.EGL_BLUE_SIZE to 16
+    EGL14.EGL_ALPHA_SIZE to 16
+    EGL14.EGL_DEPTH_SIZE to 0
+    EGL14.EGL_STENCIL_SIZE to 0
+    EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+}
+
+/**
+ * Construct an instance of [EglConfigAttributes] that includes a mapping of EGL attributes
+ * to their corresponding value. The full set of attributes can be found here:
+ * https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglChooseConfig.xhtml
+ *
+ * The resultant array of attributes automatically is terminated with EGL_NONE.
+ *
+ * For example to create an 8888 configuration, this can be done with the following:
+ *
+ * EglConfigAttributes {
+ *      EGL14.EGL_RENDERABLE_TYPE to EGL14.EGL_OPENGL_ES2_BIT
+ *      EGL14.EGL_RED_SIZE to 8
+ *      EGL14.EGL_GREEN_SIZE to 8
+ *      EGL14.EGL_BLUE_SIZE to 8
+ *      EGL14.EGL_ALPHA_SIZE to 8
+ *      EGL14.EGL_DEPTH_SIZE to 0
+ *      EGL14.EGL_CONFIG_CAVEAT to EGL14.EGL_NONE
+ *      EGL14.EGL_STENCIL_SIZE to 8
+ *      EGL14.EGL_SURFACE_TYPE to EGL14.EGL_WINDOW_BIT
+ * }
+ *
+ * @see EglConfigAttributes8888
+ */
+inline fun EglConfigAttributes(block: EglConfigAttributes.Builder.() -> Unit): EglConfigAttributes =
+    EglConfigAttributes.Builder().apply { block() }.build()
+
+// klint does not support value classes yet, see b/197692691
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class EglConfigAttributes internal constructor(
+    @PublishedApi internal val attrs: IntArray
+) {
+
+    /**
+     * Builder used to create an instance of [EglConfigAttributes]
+     * Allows for a mapping of EGL configuration attributes to their corresponding
+     * values as well as including a previously generated [EglConfigAttributes]
+     * instance to be used as a template and conditionally update individual mapped values
+     */
+    // Suppressing build method as EglConfigAttributes is created using Kotlin DSL syntax
+    // via the function constructor defined above
+    @SuppressWarnings("MissingBuildMethod")
+    class Builder @PublishedApi internal constructor() {
+        private val attrs = HashMap<Int, Int>()
+
+        /**
+         * Map a given EGL configuration attribute key to the given EGL configuration value
+         */
+        @SuppressWarnings("BuilderSetStyle")
+        infix fun Int.to(that: Int) {
+            attrs[this] = that
+        }
+
+        /**
+         * Include all the attributes of the given EglConfigAttributes instance.
+         * This is useful for creating a new EglConfigAttributes instance with all the same
+         * attributes as another, allowing for modification of attributes after the fact.
+         * For example, the following code snippet can be used to create an EglConfigAttributes
+         * instance that has all the same configuration as [EglConfigAttributes8888] but with a
+         * 16 bit stencil buffer size:
+         *
+         * EglConfigAttributes {
+         *      include(EglConfigAttributes8888)
+         *      EGL14.EGL_STENCIL_SIZE to 16
+         * }
+         *
+         *
+         * That is all attributes configured after the include will overwrite the attributes
+         * configured previously.
+         */
+        @SuppressWarnings("BuilderSetStyle")
+        fun include(attributes: EglConfigAttributes) {
+            val attrsArray = attributes.attrs
+            for (i in 0 until attrsArray.size - 1 step 2) {
+                attrs[attrsArray[i]] = attrsArray[i + 1]
+            }
+        }
+
+        @PublishedApi internal fun build(): EglConfigAttributes {
+            val entries = attrs.entries
+            val attrArray = IntArray(entries.size * 2 + 1) // Array must end with EGL_NONE
+            var index = 0
+            for (entry in entries) {
+                attrArray[index] = entry.key
+                attrArray[index + 1] = entry.value
+                index += 2
+            }
+            attrArray[index] = EGL14.EGL_NONE
+            return EglConfigAttributes(attrArray)
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt
new file mode 100644
index 0000000..1c7cdca
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglExtensions.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.graphics.opengl.egl
+
+/**
+ * Determines if applications can query the age of the back buffer contents for an
+ * EGL surface as the number of frames elapsed since the contents were recently defined
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_buffer_age.txt
+ */
+const val EglExtBufferAge = "EGL_EXT_buffer_age"
+
+/**
+ * Allows for efficient partial updates to an area of a **buffer** that has changed since
+ * the last time the buffer was used
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_partial_update.txt
+ */
+const val EglKhrPartialUpdate = "EGL_KHR_partial_update"
+
+/**
+ * Allows for efficient partial updates to an area of a **surface** that changes between
+ * frames for the surface. This relates to the differences between two buffers, the current
+ * back buffer and the current front buffer.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_swap_buffers_with_damage.txt
+ */
+const val EglKhrSwapBuffersWithDamage = "EGL_KHR_swap_buffers_with_damage"
+
+/**
+ * Determines whether to use sRGB format default framebuffers to render sRGB
+ * content to display devices. Supports creation of EGLSurfaces which will be rendered to in
+ * sRGB by OpenGL contexts supporting that capability.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
+ */
+const val EglKhrGlColorSpace = "EGL_KHR_gl_colorspace"
+
+/**
+ * Determines whether creation of GL and ES contexts without an EGLConfig is allowed
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_no_config_context.txt
+ */
+const val EglKhrNoConfigContext = "EGL_KHR_no_config_context"
+
+/**
+ * Determines whether floating point RGBA components are supported
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_pixel_format_float.txt
+ */
+const val EglExtPixelFormatFloat = "EGL_EXT_pixel_format_float"
+
+/**
+ * Determines whether extended sRGB color spaces are supported options for EGL Surfaces
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_scrgb.txt
+ */
+const val EglExtGlColorSpaceScRgb = "EGL_EXT_gl_colorspace_scrgb"
+
+/**
+ * Determines whether the underlying platform can support rendering framebuffers in the
+ * non-linear Display-P3 color space
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_display_p3_passthrough.txt
+ */
+const val EglExtColorSpaceDisplayP3Passthrough = "EGL_EXT_gl_colorspace_display_p3_passthrough"
+
+/**
+ * Determines whether the platform framebuffers support rendering in a larger color gamut
+ * specified in the BT.2020 color space
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
+ */
+const val EglExtGlColorSpaceBt2020Pq = "EGL_EXT_gl_colorspace_bt2020_pq"
+
+/**
+ * Determines whether an EGLContext can be created with a priority hint. Not all implementations
+ * are guaranteed to honor the hint.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/IMG/EGL_IMG_context_priority.txt
+ */
+const val EglImgContextPriority = "EGL_IMG_context_priority"
+
+/**
+ * Determines whether creation of an EGL Context without a surface is supported.
+ * This is useful for applications that only want to render to client API targets (such as
+ * OpenGL framebuffer objects) and avoid the need to a throw-away EGL surface just to get
+ * a current context.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
+ */
+const val EglKhrSurfacelessContext = "EGL_KHR_surfaceless_context"
+
+/**
+ * Determines whether sync objects are supported. Sync objects are synchronization primitives
+ * that represent events whose completion can be tested or waited upon.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_fence_sync.txt
+ */
+const val EglKhrFenceSync = "EGL_KHR_fence_sync"
+
+/**
+ * Determines whether waiting for signaling of sync objects is supported. This form of wait
+ * does not necessarily block the application thread which issued the wait. Therefore
+ * applications may continue to issue commands to the client API or perform other work
+ * in parallel leading to increased performance.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_wait_sync.txt
+ */
+const val EglKhrWaitSync = "EGL_KHR_wait_sync"
+
+/**
+ * Determines whether creation of platform specific sync objects are supported. These
+ * objects that are associated with a native synchronization fence object using a file
+ * descriptor.
+ *
+ * See:
+ * https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_native_fence_sync.txt
+ */
+const val EglAndroidNativeFenceSync = "EGL_ANDROID_native_fence_sync"
+
+/**
+ * Class determining the types of OpenGL extensions supported by the given
+ * EGL spec.
+ */
+// klint does not support value classes yet, see b/197692691
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class EglExtensions(
+    private val extensionSet: Set<String>
+) {
+
+    /**
+     * Determines whether the extension with the provided name is supported. The string
+     * provided is expected to be one of the named extensions defined within the OpenGL
+     * extension documentation.
+     *
+     * Returns true if the extension is supported, false otherwise
+     */
+    fun isExtensionSupported(extensionName: String): Boolean =
+        extensionSet.contains(extensionName)
+
+    companion object {
+
+        /**
+         * Creates an instance of [EglExtensions] from a space separated string
+         * that represents the set of OpenGL extensions supported
+         */
+        @JvmStatic
+        fun from(queryString: String): EglExtensions =
+            HashSet<String>().let {
+                it.addAll(queryString.split(' '))
+                EglExtensions(it)
+            }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt
new file mode 100644
index 0000000..da3e7c5
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglManager.kt
@@ -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.graphics.opengl.egl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+
+/**
+ * Class responsible for configuration of EGL related resources. This includes
+ * initialization of the corresponding EGL Display as well as EGL Context, among
+ * other EGL related facilities.
+ */
+class EglManager(val eglSpec: EglSpec = EglSpec.Egl14) {
+
+    private var mEglConfig: EGLConfig? = null
+
+    /**
+     * Offscreen pixel buffer surface
+     */
+    private var mPBufferSurface: EGLSurface = EGL14.EGL_NO_SURFACE
+    private var mEglContext: EGLContext = EGL14.EGL_NO_CONTEXT
+    private var mWideColorGamutSupport = false
+    private var mEglVersion = EglVersion.Unknown
+    private var mEglExtensions: EglExtensions? = null
+    private var mIsSingleBuffered: Boolean = false
+    private var mQueryResult: IntArray? = null
+
+    /**
+     * Initialize the EGLManager. This initializes the default display as well
+     * as queries the supported extensions
+     */
+    fun initialize() {
+        mEglContext.let {
+            if (it === EGL14.EGL_NO_CONTEXT) {
+                mEglVersion = eglSpec.eglInitialize()
+                mEglExtensions = EglExtensions.from(eglSpec.eglQueryString(EGL14.EGL_EXTENSIONS))
+            }
+        }
+    }
+
+    /**
+     * Attempt to load an [EGLConfig] instance from the given
+     * [EglConfigAttributes]. If the [EGLConfig] could not be loaded
+     * this returns null
+     */
+    fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig? =
+        eglSpec.loadConfig(configAttributes)
+
+    /**
+     * Creates an [EGLContext] from the given [EGLConfig] returning
+     * null if the context could not be created
+     *
+     * @throws EglException if the default surface could not be made current after context creation
+     */
+    fun createContext(config: EGLConfig): EGLContext {
+        val eglContext = eglSpec.eglCreateContext(config)
+        if (eglContext !== EGL14.EGL_NO_CONTEXT) {
+            val pbBufferSurface: EGLSurface = if (isExtensionSupported(EglKhrSurfacelessContext)) {
+                EGL14.EGL_NO_SURFACE
+            } else {
+                val configAttrs = EglConfigAttributes {
+                    EGL14.EGL_WIDTH to 1
+                    EGL14.EGL_HEIGHT to 1
+                }
+                eglSpec.eglCreatePBufferSurface(config, configAttrs)
+            }
+            if (!eglSpec.eglMakeCurrent(eglContext, pbBufferSurface, pbBufferSurface)) {
+                throw EglException(eglSpec.eglGetError(), "Unable to make default surface current")
+            }
+            mPBufferSurface = pbBufferSurface
+            mEglContext = eglContext
+            mEglConfig = config
+        } else {
+            mPBufferSurface = EGL14.EGL_NO_SURFACE
+            mEglContext = EGL14.EGL_NO_CONTEXT
+            mEglConfig = null
+        }
+        return eglContext
+    }
+
+    /**
+     * Release the resources allocated by EGLManager. This will destroy the corresponding
+     * EGLContext instance if it was previously initialized.
+     * The configured EGLVersion as well as EGLExtensions
+     */
+    fun release() {
+        mEglContext.let {
+            if (it != EGL14.EGL_NO_CONTEXT) {
+                eglSpec.eglDestroyContext(it)
+                mPBufferSurface.let { pbBufferSurface ->
+                    if (pbBufferSurface != EGL14.EGL_NO_SURFACE) {
+                        eglSpec.eglDestroySurface(pbBufferSurface)
+                    }
+                }
+                mPBufferSurface = EGL14.EGL_NO_SURFACE
+                eglSpec.eglMakeCurrent(
+                    EGL14.EGL_NO_CONTEXT,
+                    EGL14.EGL_NO_SURFACE,
+                    EGL14.EGL_NO_SURFACE
+                )
+                mEglVersion = EglVersion.Unknown
+                mEglContext = EGL14.EGL_NO_CONTEXT
+                mEglConfig = null
+                mEglExtensions = null
+            }
+        }
+    }
+
+    /**
+     * Returns the EGL version that is supported. This parameter is configured
+     * after [initialize] is invoked.
+     */
+    val eglVersion: EglVersion
+        get() = mEglVersion
+
+    /**
+     * Returns the current EGLContext. This parameter is configured after [initialize] is invoked
+     */
+    val eglContext: EGLContext?
+        get() = mEglContext
+
+    /**
+     * Returns the [EGLConfig] used to load the current [EGLContext].
+     * This is configured after [createContext] is invoked.
+     */
+    val eglConfig: EGLConfig?
+        get() = mEglConfig
+
+    /**
+     * Determines whether the extension with the provided name is supported. The string
+     * provided is expected to be one of the named extensions defined within the OpenGL
+     * extension documentation.
+     *
+     * See [EglExtensions] for additional documentation for given extension name constants
+     * and descriptions.
+     *
+     * The set of supported extensions is configured after [initialize] is invoked.
+     * Attempts to query support for any extension beforehand will return false.
+     */
+    fun isExtensionSupported(extensionName: String): Boolean =
+        mEglExtensions?.isExtensionSupported(extensionName) ?: false
+
+    /**
+     * Binds the current context to the given draw and read surfaces.
+     * The draw surface is used for all operations except for any pixel data read back or
+     * copy operations which are taken from the read surface.
+     *
+     * The same EGLSurface may be specified for both draw and read surfaces.
+     *
+     * If the context is not previously configured, the only valid parameters for the
+     * draw and read surfaces is [EGL14.EGL_NO_SURFACE]. This is useful to make sure there is
+     * always a surface specified and to release the current context without assigning a new one.
+     *
+     * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml
+     *
+     * @param drawSurface Surface used for all operations that involve writing pixel information
+     * @param readSurface Surface used for pixel data read back or copy operations. By default this
+     * is the same as [drawSurface]
+     */
+    @JvmOverloads
+    fun makeCurrent(drawSurface: EGLSurface, readSurface: EGLSurface = drawSurface): Boolean {
+        val result = eglSpec.eglMakeCurrent(mEglContext, drawSurface, readSurface)
+        if (result) {
+            querySurface(drawSurface)
+        }
+        return result
+    }
+
+    /**
+     * Post EGL surface color buffer to a native window. If the current drawing surface
+     * is single buffered this will flush the buffer
+     */
+    fun swapAndFlushBuffers() {
+        if (mIsSingleBuffered) {
+            GLES20.glFlush()
+        }
+        eglSpec.eglSwapBuffers(currentDrawSurface)
+    }
+
+    /**
+     * Returns the default surface. This can be an offscreen pixel buffer surface or
+     * [EGL14.EGL_NO_SURFACE] if the surfaceless context extension is supported.
+     */
+    val defaultSurface: EGLSurface
+        get() = mPBufferSurface
+
+    /**
+     * Returns the current surface used for drawing pixel content
+     */
+    val currentDrawSurface: EGLSurface
+        get() = eglSpec.eglGetCurrentDrawSurface()
+
+    /**
+     * Returns the current surface used for reading back or copying pixels
+     */
+    val currentReadSurface: EGLSurface
+        get() = eglSpec.eglGetCurrentReadSurface()
+
+    /**
+     * Helper method to query properties of the given surface
+     */
+    private fun querySurface(surface: EGLSurface) {
+        val resultArray = mQueryResult ?: IntArray(1).also { mQueryResult = it }
+        if (eglSpec.eglQuerySurface(surface, EGL14.EGL_RENDER_BUFFER, resultArray, 0)) {
+            mIsSingleBuffered = resultArray[0] == EGL14.EGL_SINGLE_BUFFER
+        }
+    }
+
+    companion object {
+        private const val TAG = "EglManager"
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt
new file mode 100644
index 0000000..b1e9881
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EglSpec.kt
@@ -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.graphics.opengl.egl
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLSurface
+import android.view.Surface
+
+/**
+ * Interface for accessing various EGL facilities independent of EGL versions.
+ * That is each EGL version implements this specification.
+ *
+ * EglSpec is not thread safe and is up to the caller of these methods to guarantee thread safety.
+ */
+interface EglSpec {
+
+    /**
+     * Query for the capabilities associated with the given eglDisplay.
+     * The result contains a space separated list of the capabilities.
+     *
+     * @param nameId identifier for the EGL string to query
+     */
+    fun eglQueryString(nameId: Int): String
+
+    /**
+     * Create a Pixel Buffer surface with the corresponding [EglConfigAttributes].
+     * Accepted attributes are defined as part of the OpenGL specification here:
+     * https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
+     *
+     * If a pixel buffer surface could not be created, [EGL14.EGL_NO_SURFACE] is returned.
+     *
+     * @param config Specifies the EGL Frame buffer configuration that defines the frame buffer
+     * resource available to the surface
+     * @param configAttributes Optional list of attributes for the pixel buffer surface
+     */
+    fun eglCreatePBufferSurface(
+        config: EGLConfig,
+        configAttributes: EglConfigAttributes?
+    ): EGLSurface
+
+    /**
+     * Creates an on screen EGL window surface from the given [Surface] and returns a handle to it.
+     *
+     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
+     *
+     * @param config Specifies the EGL frame buffer configuration that defines the frame buffer
+     * resource available to the surface
+     * @param surface Android surface to consume rendered content
+     * @param configAttributes Optional list of attributes for the specified surface
+     */
+    fun eglCreateWindowSurface(
+        config: EGLConfig,
+        surface: Surface,
+        configAttributes: EglConfigAttributes?
+    ): EGLSurface
+
+    /**
+     * Destroys an EGL surface.
+     *
+     * If the EGL surface is not current to any thread, eglDestroySurface destroys
+     * it immediately. Otherwise, surface is destroyed when it becomes not current to any thread.
+     * Furthermore, resources associated with a pbuffer surface are not released until all color
+     * buffers of that pbuffer bound to a texture object have been released. Deferral of
+     * surface destruction would still return true as deferral does not indicate a failure condition
+     *
+     * @return True if destruction of the EGLSurface was successful, false otherwise
+     */
+    fun eglDestroySurface(surface: EGLSurface): Boolean
+
+    /**
+     * Binds the current context to the given draw and read surfaces.
+     * The draw surface is used for all operations except for any pixel data read back or copy
+     * operations which are taken from the read surface.
+     *
+     * The same EGLSurface may be specified for both draw and read surfaces.
+     *
+     * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml for more
+     * information
+     *
+     * @param drawSurface EGLSurface to draw pixels into.
+     * @param readSurface EGLSurface used for read/copy operations.
+     */
+    fun eglMakeCurrent(
+        context: EGLContext,
+        drawSurface: EGLSurface,
+        readSurface: EGLSurface
+    ): Boolean
+
+    /**
+     * Return the current surface used for reading or copying pixels.
+     * If no context is current, [EGL14.EGL_NO_SURFACE] is returned
+     */
+    fun eglGetCurrentReadSurface(): EGLSurface
+
+    /**
+     * Return the current surface used for drawing pixels.
+     * If no context is current, [EGL14.EGL_NO_SURFACE] is returned.
+     */
+    fun eglGetCurrentDrawSurface(): EGLSurface
+
+    /**
+     * Initialize the EGL implementation and return the major and minor version of the EGL
+     * implementation through [EglVersion]. If initialization fails, this returns
+     * [EglVersion.Unknown]
+     */
+    fun eglInitialize(): EglVersion
+
+    /**
+     * Load a corresponding EGLConfig from the provided [EglConfigAttributes]
+     * If the EGLConfig could not be loaded, null is returned
+     * @param configAttributes Desired [EglConfigAttributes] to create an [EGLConfig]
+     *
+     * @return the [EGLConfig] with the provided [EglConfigAttributes] or null if
+     * an [EGLConfig] could not be created with the specified attributes
+     */
+    fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig?
+
+    /**
+     * Create an EGLContext with the default display. If createContext fails to create a
+     * rendering context, EGL_NO_CONTEXT is returned
+     *
+     * @param config [EGLConfig] used to create the [EGLContext]
+     */
+    fun eglCreateContext(config: EGLConfig): EGLContext
+
+    /**
+     * Destroy the given EGLContext generated in [eglCreateContext]
+     *
+     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglDestroyContext.xhtml
+     *
+     * @param eglContext EGL rendering context to be destroyed
+     */
+    fun eglDestroyContext(eglContext: EGLContext)
+
+    /**
+     * Post EGL surface color buffer to a native window
+     *
+     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglSwapBuffers.xhtml
+     *
+     * @param surface Specifies the EGL drawing surface whose buffers are to be swapped
+     *
+     * @return True if swapping of buffers succeeds, false otherwise
+     */
+    fun eglSwapBuffers(surface: EGLSurface): Boolean
+
+    /**
+     * Query the EGL attributes of the provided surface
+     *
+     * @param surface EGLSurface to be queried
+     * @param attribute EGL attribute to query on the given EGL Surface
+     * @param result Int array to store the result of the query
+     * @param offset Index within [result] to store the value of the queried attribute
+     *
+     * @return True if the query was completed successfully, false otherwise. If the query
+     * fails, [result] is unmodified
+     */
+    fun eglQuerySurface(surface: EGLSurface, attribute: Int, result: IntArray, offset: Int): Boolean
+
+    /**
+     * Returns the error of the last called EGL function in the current thread. Initially,
+     * the error is set to EGL_SUCCESS. When an EGL function could potentially generate several
+     * different errors (for example, when passed both a bad attribute name, and a bad attribute
+     * value for a legal attribute name), the implementation may choose to generate any one of the
+     * applicable errors.
+     *
+     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglGetError.xhtml for more information
+     * and error codes that could potentially be returned
+     */
+    fun eglGetError(): Int
+
+    /**
+     * Convenience method to obtain the corresponding error string from the
+     * error code obtained from [EglSpec.eglGetError]
+     */
+    fun getErrorMessage(): String = getStatusString(eglGetError())
+
+    companion object {
+
+        @JvmField
+        val Egl14 = object : EglSpec {
+
+            // Tuples of attribute identifiers along with their corresponding values.
+            // EGL_NONE is used as a termination value similar to a null terminated string
+            private val contextAttributes = intArrayOf(
+                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, // GLES VERSION 2
+                // HWUI provides the ability to configure a context priority as well but that only
+                // seems to be configured on SystemUIApplication. This might be useful for
+                // front buffer rendering situations for performance.
+                EGL14.EGL_NONE
+            )
+
+            override fun eglInitialize(): EglVersion {
+                // eglInitialize is destructive so create 2 separate arrays to store the major and
+                // minor version
+                val major = intArrayOf(1)
+                val minor = intArrayOf(1)
+                val initializeResult =
+                    EGL14.eglInitialize(getDefaultDisplay(), major, 0, minor, 0)
+                if (initializeResult) {
+                    return EglVersion(major[0], minor[0])
+                } else {
+                    throw EglException(EGL14.eglGetError(), "Unable to initialize default display")
+                }
+            }
+
+            override fun eglGetCurrentReadSurface(): EGLSurface =
+                EGL14.eglGetCurrentSurface(EGL14.EGL_READ)
+
+            override fun eglGetCurrentDrawSurface(): EGLSurface =
+                EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)
+
+            override fun eglQueryString(nameId: Int): String =
+                EGL14.eglQueryString(getDefaultDisplay(), nameId)
+
+            override fun eglCreatePBufferSurface(
+                config: EGLConfig,
+                configAttributes: EglConfigAttributes?
+            ): EGLSurface =
+                EGL14.eglCreatePbufferSurface(
+                    getDefaultDisplay(),
+                    config,
+                    configAttributes?.attrs,
+                    0
+                )
+
+            override fun eglCreateWindowSurface(
+                config: EGLConfig,
+                surface: Surface,
+                configAttributes: EglConfigAttributes?,
+            ): EGLSurface =
+                EGL14.eglCreateWindowSurface(
+                    getDefaultDisplay(),
+                    config,
+                    surface,
+                    configAttributes?.attrs ?: DefaultWindowSurfaceConfig.attrs,
+                    0
+                )
+
+            override fun eglSwapBuffers(surface: EGLSurface): Boolean =
+                EGL14.eglSwapBuffers(getDefaultDisplay(), surface)
+
+            override fun eglQuerySurface(
+                surface: EGLSurface,
+                attribute: Int,
+                result: IntArray,
+                offset: Int
+            ): Boolean =
+                EGL14.eglQuerySurface(getDefaultDisplay(), surface, attribute, result, offset)
+
+            override fun eglDestroySurface(surface: EGLSurface) =
+                EGL14.eglDestroySurface(getDefaultDisplay(), surface)
+
+            override fun eglMakeCurrent(
+                context: EGLContext,
+                drawSurface: EGLSurface,
+                readSurface: EGLSurface
+            ): Boolean =
+                EGL14.eglMakeCurrent(
+                    getDefaultDisplay(),
+                    drawSurface,
+                    readSurface,
+                    context
+                )
+
+            override fun loadConfig(configAttributes: EglConfigAttributes): EGLConfig? {
+                val configs = arrayOfNulls<EGLConfig?>(1)
+                return if (EGL14.eglChooseConfig(
+                    getDefaultDisplay(),
+                    configAttributes.attrs,
+                    0,
+                    configs,
+                    0,
+                    1,
+                    intArrayOf(1),
+                    0
+                )) {
+                    configs[0]
+                } else {
+                    null
+                }
+            }
+
+            override fun eglCreateContext(config: EGLConfig): EGLContext {
+                return EGL14.eglCreateContext(
+                    getDefaultDisplay(),
+                    config,
+                    EGL14.EGL_NO_CONTEXT, // not creating from a shared context
+                    contextAttributes,
+                    0
+                )
+            }
+
+            override fun eglDestroyContext(eglContext: EGLContext) {
+                if (!EGL14.eglDestroyContext(getDefaultDisplay(), eglContext)) {
+                    throw EglException(EGL14.eglGetError(), "Unable to destroy EGLContext")
+                }
+            }
+
+            override fun eglGetError(): Int = EGL14.eglGetError()
+
+            private fun getDefaultDisplay() = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+
+            /**
+             * EglConfigAttribute that provides the default attributes for an EGL window surface
+             */
+            private val DefaultWindowSurfaceConfig = EglConfigAttributes {}
+        }
+
+        /**
+         * Return a string representation of the corresponding EGL status code.
+         * If the provided error value is not an EGL status code, the hex representation
+         * is returned instead
+         */
+        @JvmStatic
+        fun getStatusString(error: Int): String =
+            when (error) {
+                EGL14.EGL_SUCCESS -> "EGL_SUCCESS"
+                EGL14.EGL_NOT_INITIALIZED -> "EGL_NOT_INITIALIZED"
+                EGL14.EGL_BAD_ACCESS -> "EGL_BAD_ACCESS"
+                EGL14.EGL_BAD_ALLOC -> "EGL_BAD_ALLOC"
+                EGL14.EGL_BAD_ATTRIBUTE -> "EGL_BAD_ATTRIBUTE"
+                EGL14.EGL_BAD_CONFIG -> "EGL_BAD_CONFIG"
+                EGL14.EGL_BAD_CONTEXT -> "EGL_BAD_CONTEXT"
+                EGL14.EGL_BAD_CURRENT_SURFACE -> "EGL_BAD_CURRENT_SURFACE"
+                EGL14.EGL_BAD_DISPLAY -> "EGL_BAD_DISPLAY"
+                EGL14.EGL_BAD_MATCH -> "EGL_BAD_MATCH"
+                EGL14.EGL_BAD_NATIVE_PIXMAP -> "EGL_BAD_NATIVE_PIXMAP"
+                EGL14.EGL_BAD_NATIVE_WINDOW -> "EGL_BAD_NATIVE_WINDOW"
+                EGL14.EGL_BAD_PARAMETER -> "EGL_BAD_PARAMETER"
+                EGL14.EGL_BAD_SURFACE -> "EGL_BAD_SURFACE"
+                EGL14.EGL_CONTEXT_LOST -> "EGL_CONTEXT_LOST"
+                else -> Integer.toHexString(error)
+            }
+    }
+}
+
+/**
+ * Exception class for reporting errors with EGL
+ *
+ * @param error Error code reported via eglGetError
+ * @param msg Optional message describing the exception being thrown
+ */
+class EglException(val error: Int, val msg: String = "") : RuntimeException() {
+
+    override val message: String
+        get() = "Error: ${EglSpec.getStatusString(error)}, $msg"
+}
+
+/**
+ * Identifier for the current EGL implementation
+ *
+ * @param major Major version of the EGL implementation
+ * @param minor Minor version of the EGL implementation
+ */
+data class EglVersion(
+    val major: Int,
+    val minor: Int
+) {
+
+    override fun toString(): String {
+        return "EGL version $major.$minor"
+    }
+
+    companion object {
+        /**
+         * Constant that represents version 1.4 of the EGL spec
+         */
+        @JvmField
+        val V14 = EglVersion(1, 4)
+
+        /**
+         * Constant that represents version 1.5 of the EGL spec
+         */
+        @JvmField
+        val V15 = EglVersion(1, 5)
+
+        /**
+         * Sentinel EglVersion value returned in error situations
+         */
+        @JvmField
+        val Unknown = EglVersion(-1, -1)
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
new file mode 100644
index 0000000..6b86ed4
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -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.graphics.surface
+
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.Q)
+class SurfaceControlCompat(surface: Surface, debugName: String) {
+    private var mNativeSurfaceControl: Long
+
+    init {
+        mNativeSurfaceControl = nCreateFromWindow(surface, debugName)
+        if (mNativeSurfaceControl == 0L) {
+            throw IllegalArgumentException()
+        }
+    }
+
+    open class Transaction() {
+        private var mNativeSurfaceTransaction: Long
+
+        init {
+            mNativeSurfaceTransaction = nTransactionCreate()
+            if (mNativeSurfaceTransaction == 0L) {
+                throw java.lang.IllegalArgumentException()
+            }
+        }
+
+        fun delete() {
+            if (mNativeSurfaceTransaction != 0L) {
+                nTransactionDelete(mNativeSurfaceTransaction)
+            }
+            mNativeSurfaceTransaction = 0L
+        }
+
+        fun finalize() {
+            delete()
+        }
+
+        private external fun nTransactionCreate(): Long
+        private external fun nTransactionDelete(surfaceTransaction: Long)
+
+        companion object {
+            init {
+                System.loadLibrary("graphics-core")
+            }
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other == this) {
+            return true
+        }
+        if ((other == null) or
+            (other?.javaClass != SurfaceControlCompat::class.java)
+        ) {
+            return false
+        }
+
+        other as SurfaceControlCompat
+        if (other.mNativeSurfaceControl == this.mNativeSurfaceControl) {
+            return true
+        }
+
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return mNativeSurfaceControl.hashCode()
+    }
+
+    protected fun finalize() {
+        nRelease(mNativeSurfaceControl)
+        mNativeSurfaceControl = 0
+    }
+
+    private external fun nCreateFromWindow(surface: Surface, debugName: String): Long
+    private external fun nRelease(surfaceControl: Long)
+
+    companion object {
+        init {
+            System.loadLibrary("graphics-core")
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt b/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
index 92db36c..e421a6d 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/permission/HealthDataRequestPermissions.kt
@@ -49,6 +49,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     override fun parseResult(resultCode: Int, intent: Intent?): Set<Permission> {
         return intent
             ?.getParcelableArrayListExtra<ProtoPermission>(KEY_GRANTED_PERMISSIONS_JETPACK)
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
index fac68c0..1c14738 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
@@ -60,6 +60,7 @@
     private const val EXTRA_PERMISSIONS_GRANTED: String = "hs.data_points_has_permissions"
 
     /** Retrieves the [DataPoint] s that are contained in the given [Intent], if any. */
+    @Suppress("DEPRECATION")
     @JvmStatic
     @Keep
     public fun getDataPoints(intent: Intent): List<DataPoint> =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
index d4912c3..74118ef 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveGoal.kt
@@ -105,6 +105,7 @@
          * Creates a [PassiveGoal] from an [Intent]. Returns null if no [PassiveGoal] is stored in
          * the given intent.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         public fun fromIntent(intent: Intent): PassiveGoal? = intent.getParcelableExtra(EXTRA_KEY)
     }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
index c57e698..fd6b262 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveMonitoringUpdate.kt
@@ -78,6 +78,7 @@
          * Creates a [PassiveMonitoringUpdate] from an [Intent]. Returns null if no
          * [PassiveMonitoringUpdate] is stored in the given intent.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         public fun fromIntent(intent: Intent): PassiveMonitoringUpdate? =
             intent.getParcelableExtra(EXTRA_KEY)
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index fd65713..af40a5a 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -459,7 +459,11 @@
                 }
                 mWidth = mBitmaps[0].getWidth();
                 mHeight = mBitmaps[0].getHeight();
-                retriever.release();
+                try {
+                    retriever.release();
+                } catch (IOException e) {
+                    // Nothing we can  do about it.
+                }
             }
 
             private void cleanupStaleOutputs() {
@@ -702,7 +706,11 @@
         assertEquals("Wrong primary index", primary,
                 Integer.parseInt(retriever.extractMetadata(
                         MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
-        retriever.release();
+        try {
+            retriever.release();
+        } catch (IOException e) {
+            // Nothing we can  do about it.
+        }
 
         if (useGrid) {
             MediaExtractor extractor = new MediaExtractor();
diff --git a/libraryversions.toml b/libraryversions.toml
index d321fe6..5e11b1e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,9 +1,9 @@
 [versions]
-ACTIVITY = "1.5.0-alpha06"
+ACTIVITY = "1.6.0-alpha04"
 ADS_IDENTIFIER = "1.0.0-alpha05"
 ANNOTATION = "1.4.0-alpha03"
 ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
-APPCOMPAT = "1.5.0-alpha02"
+APPCOMPAT = "1.6.0-alpha04"
 APPSEARCH = "1.0.0-alpha05"
 ARCH_CORE = "2.2.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha01"
@@ -22,9 +22,9 @@
 COMPOSE_MATERIAL3 = "1.0.0-alpha10"
 CONTENTPAGER = "1.1.0-alpha01"
 COORDINATORLAYOUT = "1.3.0-alpha01"
-CORE = "1.8.0-alpha08"
-CORE_ANIMATION = "1.0.0-alpha03"
-CORE_ANIMATION_TESTING = "1.0.0-alpha03"
+CORE = "1.9.0-alpha04"
+CORE_ANIMATION = "1.0.0-beta01"
+CORE_ANIMATION_TESTING = "1.0.0-beta01"
 CORE_APPDIGEST = "1.0.0-alpha01"
 CORE_GOOGLE_SHORTCUTS = "1.1.0-alpha02"
 CORE_I18N = "1.0.0-alpha01"
@@ -50,6 +50,7 @@
 FUTURES = "1.2.0-alpha01"
 GLANCE = "1.0.0-alpha04"
 GLANCE_TEMPLATE = "1.0.0-alpha01"
+GRAPHICS = "1.0.0-alpha01"
 GRIDLAYOUT = "1.1.0-alpha01"
 HEALTH_DATA_CLIENT = "1.0.0-alpha01"
 HEALTH_SERVICES_CLIENT = "1.0.0-alpha04"
@@ -113,9 +114,9 @@
 TRACING_PERFETTO = "1.0.0-alpha01"
 TRANSITION = "1.5.0-alpha01"
 TVPROVIDER = "1.1.0-alpha02"
-VECTORDRAWABLE = "1.2.0-alpha03"
-VECTORDRAWABLE_ANIMATED = "1.2.0-alpha01"
-VECTORDRAWABLE_SEEKABLE = "1.0.0-alpha03"
+VECTORDRAWABLE = "1.2.0-beta01"
+VECTORDRAWABLE_ANIMATED = "1.2.0-beta01"
+VECTORDRAWABLE_SEEKABLE = "1.0.0-beta01"
 VERSIONED_PARCELABLE = "1.2.0-alpha01"
 VIEWPAGER = "1.1.0-alpha02"
 VIEWPAGER2 = "1.1.0-beta02"
@@ -179,6 +180,7 @@
 FRAGMENT = { group = "androidx.fragment", atomicGroupVersion = "versions.FRAGMENT" }
 GLANCE = { group = "androidx.glance", atomicGroupVersion = "versions.GLANCE" }
 GLANCE_TEMPLATE = { group = "androidx.template", atomicGroupVersion = "versions.GLANCE_TEMPLATE" }
+GRAPHICS = { group = "androidx.graphics", atomicGroupVersion = "versions.GRAPHICS" }
 GRIDLAYOUT = { group = "androidx.gridlayout", atomicGroupVersion = "versions.GRIDLAYOUT" }
 HEALTH = { group = "androidx.health" }
 HEIFWRITER = { group = "androidx.heifwriter", atomicGroupVersion = "versions.HEIFWRITER" }
diff --git a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
index 1cab987..04055db 100644
--- a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
+++ b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/ServiceLifecycleTest.java
@@ -52,6 +52,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+@SuppressWarnings("deprecation")
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class ServiceLifecycleTest {
diff --git a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index 16acd88..717e9c1 100644
--- a/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -1185,6 +1185,7 @@
          * @param mediaButtonEvent The media button event intent.
          * @return True if the event was handled, false otherwise.
          */
+        @SuppressWarnings("deprecation")
         public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
             if (android.os.Build.VERSION.SDK_INT >= 27) {
                 // Double tap of play/pause as skipping to next is already handled by framework,
diff --git a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
index 10d515c..1b26d16 100644
--- a/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
+++ b/media/media/src/main/java/androidx/media/session/MediaButtonReceiver.java
@@ -150,6 +150,7 @@
             mMediaBrowser = mediaBrowser;
         }
 
+        @SuppressWarnings("deprecation")
         @Override
         public void onConnected() {
             MediaControllerCompat mediaController = new MediaControllerCompat(mContext,
@@ -186,6 +187,7 @@
      * @param intent The intent to parse.
      * @return The extracted {@link KeyEvent} if found, or null.
      */
+    @SuppressWarnings("deprecation")
     public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) {
         if (mediaSessionCompat == null || intent == null
                 || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
diff --git a/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java b/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
index 8ff807d..f2df815 100644
--- a/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
+++ b/media2/integration-tests/testapp/src/main/java/androidx/media2/integration/testapp/VideoSessionService.java
@@ -39,6 +39,7 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -195,7 +196,11 @@
             Bitmap musicAlbumBitmap = extractAlbumArt(retriever);
 
             if (retriever != null) {
-                retriever.release();
+                try {
+                    retriever.release();
+                } catch (IOException e) {
+                    // Nothing we can do about that...
+                }
             }
 
             MediaMetadata metadata = mediaItem.getMetadata();
diff --git a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
index d2b5aa5..d694aa5 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
@@ -826,7 +826,11 @@
         retriever.setDataSource(file);
         String rotation = retriever.extractMetadata(
                 MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
-        retriever.release();
+        try {
+            retriever.release();
+        } catch (IOException e) {
+            // Nothing we can  do about it.
+        }
         retriever = null;
         assertNotNull(rotation);
         assertEquals(Integer.parseInt(rotation), angle);
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index 34f5ad0..27ec402 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -1672,6 +1672,7 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     final class MediaButtonReceiver extends BroadcastReceiver {
+        @SuppressWarnings("deprecation")
         @Override
         public void onReceive(Context context, Intent intent) {
             if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
index 5dc5374..9515408 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaSessionServiceImplBase.java
@@ -150,6 +150,7 @@
         }
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (intent == null || intent.getAction() == null) {
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
index b53c19f..602e9f0 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavDeepLinkBuilderTest.kt
@@ -184,6 +184,7 @@
         assertThat(ids).asList().containsExactly(R.id.nav_root, R.id.second_test)
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun generateExplicitStartDestinationWithArgs() {
         val deepLinkBuilder = NavDeepLinkBuilder(targetContext)
@@ -204,6 +205,7 @@
         assertThat(args).containsExactly("arg1", "arg2").inOrder()
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun generateExplicitNavRootWithArgs() {
         val deepLinkBuilder = NavDeepLinkBuilder(targetContext)
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 694d2c1..f685e85 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -307,6 +307,7 @@
                 return ProfileVersion.V010_P;
 
             case Build.VERSION_CODES.S:
+            case Build.VERSION_CODES.S_V2:
                 return ProfileVersion.V015_S;
 
             default:
@@ -339,6 +340,7 @@
 
             // The profiles for S require a typeIdCount. Therefore metadata is required.
             case Build.VERSION_CODES.S:
+            case Build.VERSION_CODES.S_V2:
                 return true;
 
             default:
diff --git a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
index 14e27aa..5fcd73a 100644
--- a/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
+++ b/recyclerview/recyclerview-selection/src/androidTest/java/androidx/recyclerview/selection/testing/TestData.java
@@ -31,8 +31,8 @@
 
     public static List<Long> createLongData(int num) {
         List<Long> items = new ArrayList<>(num);
-        for (int i = 0; i < num; ++i) {
-            items.add(new Long(i));
+        for (long i = 0; i < num; ++i) {
+            items.add(i);
         }
         return items;
     }
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
index 77e5126..3ad4b36 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaButtonReceiver.java
@@ -28,6 +28,7 @@
  * remote route volume in lock screen. It routes media key events back
  * to main app activity SampleMediaRouterActivity.
  */
+@SuppressWarnings("deprecation")
 public class SampleMediaButtonReceiver extends BroadcastReceiver {
     private static final String TAG = "SampleMediaButtonReceiver";
     private static SampleMediaRouterActivity mActivity;
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
index e695d7d..afe07ed9 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouteProvider.java
@@ -51,6 +51,7 @@
  *
  * @see SampleMediaRouteProviderService
  */
+@SuppressWarnings("deprecation")
 class SampleMediaRouteProvider extends MediaRouteProvider {
     private static final String TAG = "SampleMrp";
 
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index 6f72b20..507d14e 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -78,6 +78,7 @@
  * targets.
  * </p>
  */
+@SuppressWarnings("deprecation")
 public class SampleMediaRouterActivity extends AppCompatActivity {
     private static final String TAG = "SampleMediaRouter";
     private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment";
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
index 6e2cbb4d..3f218ab 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsActivity.java
@@ -16,6 +16,7 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+@SuppressWarnings("deprecation")
 public class DetailsActivity extends Activity
 {
     public static final String EXTRA_ITEM = "item";
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
index f1a5ceb..8930028 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportActivity.java
@@ -20,6 +20,7 @@
 
 import androidx.fragment.app.FragmentActivity;
 
+@SuppressWarnings("deprecation")
 public class DetailsSupportActivity extends FragmentActivity
 {
     public static final String EXTRA_ITEM = "item";
diff --git a/settings.gradle b/settings.gradle
index 58d4a8c..f25ddc5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -51,8 +51,9 @@
             value("BUILD_NUMBER", buildNumber)
             link("ci.android.com build", "https://ci.android.com/builds/branches/aosp-androidx-main/grid?head=$buildNumber&tail=$buildNumber")
         }
-        publishAlways()
-        publishIfAuthenticated()
+        // Do not publish scan for androidx-platform-dev
+        // publishAlways()
+        // publishIfAuthenticated()
     }
 }
 
@@ -544,6 +545,7 @@
 includeProject(":health:health-data-client", [BuildType.MAIN])
 includeProject(":health:health-services-client", [BuildType.MAIN])
 includeProject(":heifwriter:heifwriter", [BuildType.MAIN])
+includeProject(":graphics:graphics-core", [BuildType.MAIN])
 includeProject(":hilt:hilt-common", [BuildType.MAIN])
 includeProject(":hilt:hilt-compiler", [BuildType.MAIN])
 includeProject(":hilt:hilt-navigation", [BuildType.MAIN, BuildType.COMPOSE])
@@ -796,7 +798,7 @@
 includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":window:sidecar:sidecar", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
-includeProject(":window:window-java", [BuildType.MAIN])
+includeProject(":window:window-java", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":window:window-rxjava2", [BuildType.MAIN])
 includeProject(":window:window-rxjava3", [BuildType.MAIN])
 includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
diff --git a/text/text/build.gradle b/text/text/build.gradle
index 1fd3a06..adbeebc 100644
--- a/text/text/build.gradle
+++ b/text/text/build.gradle
@@ -25,6 +25,7 @@
 
 dependencies {
     implementation(libs.kotlinStdlib)
+    implementation("androidx.core:core:1.7.0")
 
     api "androidx.annotation:annotation:1.2.0"
 
@@ -32,7 +33,6 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.junit)
 
-    androidTestImplementation("androidx.core:core:1.5.0-rc02")
     androidTestImplementation(project(":compose:ui:ui-test-font"))
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.testRunner)
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
index e4a4227..fba9e8f 100644
--- a/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/BoringLayoutFactoryTest.kt
@@ -74,12 +74,42 @@
     }
 
     @Test
-    fun create_returnsGivenValues() {
+    fun create_returnsGivenValues_includePadding_false() {
         val text = "abc"
         val paint = TextPaint()
         val width = 100
         val metrics = BoringLayout.isBoring(text, paint)
-        val boringLayout = create(text, paint, width, metrics)
+        val boringLayout = create(
+            text = text,
+            paint = paint,
+            width = width,
+            metrics = metrics,
+            includePadding = false
+        )
+
+        assertThat(boringLayout.text).isEqualTo(text)
+        assertThat(boringLayout.paint).isEqualTo(paint)
+        // The width and height of the boringLayout is the same in metrics, indicating metrics is
+        // passed correctly.
+        assertThat(boringLayout.getLineWidth(0).toInt()).isEqualTo(metrics.width)
+        assertThat(boringLayout.getLineBottom(0) - boringLayout.getLineTop(0))
+            .isEqualTo(metrics.descent - metrics.ascent)
+        assertThat(boringLayout.width).isEqualTo(width)
+    }
+
+    @Test
+    fun create_returnsGivenValues_includePadding_true() {
+        val text = "abc"
+        val paint = TextPaint()
+        val width = 100
+        val metrics = BoringLayout.isBoring(text, paint)
+        val boringLayout = create(
+            text = text,
+            paint = paint,
+            width = width,
+            metrics = metrics,
+            includePadding = true
+        )
 
         assertThat(boringLayout.text).isEqualTo(text)
         assertThat(boringLayout.paint).isEqualTo(paint)
@@ -137,7 +167,7 @@
     }
 
     @Test
-    fun create_defaultIncludePad_isTrue() {
+    fun create_defaultIncludePad_isFalse() {
         val text: CharSequence = "abcdefghijk"
         val paint = TextPaint()
         val metrics = BoringLayout.isBoring(text, paint)
@@ -150,8 +180,8 @@
 
         val topPad = boringLayout.topPadding
         val bottomPad = boringLayout.bottomPadding
-        // Top and bottom padding are not 0 at the same time, indicating includePad is true.
-        assertThat(topPad * topPad + bottomPad * bottomPad).isGreaterThan(0)
+        // Top and bottom padding are 0 at the same time, indicating includePad is false
+        assertThat(topPad + bottomPad).isEqualTo(0)
     }
 
     @Test(expected = IllegalArgumentException::class)
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java
new file mode 100644
index 0000000..b1379e8
--- /dev/null
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutConstructor33.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.compose.ui.text.android;
+
+import android.text.BoringLayout;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Platform BoringLayout constructor has marked TextUtils.TruncateAt as NonNull even though it is
+ * nullable, and have to be nullable.
+ *
+ * This class has the same signature of the BoringLayout constructor with only difference of
+ * ellipsize is marked as Nullable.
+ *
+ * This was the only way to prevent compilation failure for nullability of TruncateAt ellipsize.
+ *
+ * See b/225695033
+ */
+@RequiresApi(33)
+class BoringLayoutConstructor33 {
+
+    private BoringLayoutConstructor33() {}
+
+    @NonNull
+    public static BoringLayout create(
+            @NonNull CharSequence text,
+            @NonNull TextPaint paint,
+            @IntRange(from = 0) int width,
+            @NonNull Layout.Alignment alignment,
+            float lineSpacingMultiplier,
+            float lineSpacingExtra,
+            @NonNull BoringLayout.Metrics metrics,
+            boolean includePadding,
+            @Nullable TextUtils.TruncateAt ellipsize,
+            @IntRange(from = 0) int ellipsizedWidth,
+            boolean useFallbackLineSpacing
+    ) {
+        return new BoringLayout(
+                text,
+                paint,
+                width,
+                alignment,
+                lineSpacingMultiplier,
+                lineSpacingExtra,
+                metrics,
+                includePadding,
+                ellipsize,
+                ellipsizedWidth,
+                useFallbackLineSpacing
+        );
+    }
+}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
index f4300cb..f9116ac 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/BoringLayoutFactory.kt
@@ -18,9 +18,13 @@
 import android.text.BoringLayout
 import android.text.BoringLayout.Metrics
 import android.text.Layout.Alignment
+import android.text.StaticLayout
 import android.text.TextDirectionHeuristic
 import android.text.TextPaint
 import android.text.TextUtils.TruncateAt
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.core.os.BuildCompat
 
 /**
  * Factory Class for BoringLayout
@@ -28,7 +32,7 @@
 @OptIn(InternalPlatformTextApi::class)
 internal object BoringLayoutFactory {
     /**
-     * Try to lay out text by BoringLayout with provided paint and text direction.
+     * Try to layout text by BoringLayout with provided paint and text direction.
      *
      * @param text the text to analyze.
      * @param paint TextPaint which carries text style parameters such as size, weight, font e.g.
@@ -36,15 +40,16 @@
      * @return null if not boring; the width, ascent, and descent in a BoringLayout.Metrics
      * object.
      */
+    @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
     fun measure(
         text: CharSequence,
-        paint: TextPaint?,
+        paint: TextPaint,
         textDir: TextDirectionHeuristic
     ): Metrics? {
-        return if (!textDir.isRtl(text, 0, text.length)) {
-            BoringLayout.isBoring(text, paint, null /* metrics */)
+        return if (BuildCompat.isAtLeastT()) {
+            BoringLayoutFactory33.isBoring(text, paint, textDir)
         } else {
-            null
+            BoringLayoutFactoryDefault.isBoring(text, paint, textDir)
         }
     }
 
@@ -58,23 +63,33 @@
      * @param alignment To which edge the text is aligned.
      * @param includePadding Whether to add extra space beyond font ascent and descent (which is
      * needed to avoid clipping in some languages, such as Arabic and Kannada). Default is true.
+     * @param useFallbackLineSpacing Sets Android TextView#setFallbackLineSpacing. This value should
+     * be set to true in most cases and it is the default on platform; otherwise tall scripts such
+     * as Burmese or Tibetan result in clippings on top and bottom sometimes making the text
+     * not-readable.
      * @param ellipsize The ellipsize option specifying how the overflowed text is handled.
      * @param ellipsizedWidth The width where the exceeding text will be ellipsized, in pixel.
+     *
+     * @see BoringLayout.isFallbackLineSpacingEnabled
+     * @see StaticLayout.Builder.setUseLineSpacingFromFallbacks
      **/
+    @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
     fun create(
         text: CharSequence,
         paint: TextPaint,
         width: Int,
         metrics: Metrics,
         alignment: Alignment = Alignment.ALIGN_NORMAL,
-        includePadding: Boolean = true,
+        includePadding: Boolean = LayoutCompat.DEFAULT_INCLUDE_PADDING,
+        useFallbackLineSpacing: Boolean = LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
         ellipsize: TruncateAt? = null,
-        ellipsizedWidth: Int = width
+        ellipsizedWidth: Int = width,
     ): BoringLayout {
         require(width >= 0)
         require(ellipsizedWidth >= 0)
-        return if (ellipsize == null) {
-            BoringLayout(
+
+        return if (BuildCompat.isAtLeastT()) {
+            BoringLayoutFactory33.create(
                 text,
                 paint,
                 width,
@@ -82,10 +97,13 @@
                 LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER,
                 LayoutCompat.DEFAULT_LINESPACING_EXTRA,
                 metrics,
-                includePadding
+                includePadding,
+                useFallbackLineSpacing,
+                ellipsize,
+                ellipsizedWidth
             )
         } else {
-            BoringLayout(
+            BoringLayoutFactoryDefault.create(
                 text,
                 paint,
                 width,
@@ -99,4 +117,107 @@
             )
         }
     }
+
+    /**
+     * Returns whether fallbackLineSpacing is enabled for the given layout.
+     */
+    @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+    fun isFallbackLineSpacingEnabled(layout: BoringLayout): Boolean {
+        return if (BuildCompat.isAtLeastT()) {
+            BoringLayoutFactory33.isFallbackLineSpacingEnabled(layout)
+        } else {
+            return false
+        }
+    }
+}
+
+@RequiresApi(33)
+@OptIn(InternalPlatformTextApi::class)
+private object BoringLayoutFactory33 {
+
+    @JvmStatic
+    @DoNotInline
+    fun isBoring(text: CharSequence, paint: TextPaint, textDir: TextDirectionHeuristic): Metrics? {
+        return BoringLayout.isBoring(
+            text,
+            paint,
+            textDir,
+            LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
+            null /* metrics */
+        )
+    }
+
+    @JvmStatic
+    @DoNotInline
+    fun create(
+        text: CharSequence,
+        paint: TextPaint,
+        width: Int,
+        alignment: Alignment,
+        lineSpacingMultiplier: Float,
+        lineSpacingExtra: Float,
+        metrics: Metrics,
+        includePadding: Boolean,
+        useFallbackLineSpacing: Boolean,
+        ellipsize: TruncateAt? = null,
+        ellipsizedWidth: Int = width
+    ): BoringLayout {
+        return BoringLayoutConstructor33.create(
+            text,
+            paint,
+            width,
+            alignment,
+            lineSpacingMultiplier,
+            lineSpacingExtra,
+            metrics,
+            includePadding,
+            ellipsize,
+            ellipsizedWidth,
+            useFallbackLineSpacing
+        )
+    }
+
+    fun isFallbackLineSpacingEnabled(layout: BoringLayout): Boolean {
+        return layout.isFallbackLineSpacingEnabled
+    }
+}
+
+private object BoringLayoutFactoryDefault {
+    @JvmStatic
+    @DoNotInline
+    fun isBoring(text: CharSequence, paint: TextPaint, textDir: TextDirectionHeuristic): Metrics? {
+        return if (!textDir.isRtl(text, 0, text.length)) {
+            return BoringLayout.isBoring(text, paint, null /* metrics */)
+        } else {
+            null
+        }
+    }
+
+    @JvmStatic
+    @DoNotInline
+    fun create(
+        text: CharSequence,
+        paint: TextPaint,
+        width: Int,
+        alignment: Alignment,
+        lineSpacingMultiplier: Float,
+        lineSpacingExtra: Float,
+        metrics: Metrics,
+        includePadding: Boolean,
+        ellipsize: TruncateAt? = null,
+        ellipsizedWidth: Int = width
+    ): BoringLayout {
+        return BoringLayout(
+            text,
+            paint,
+            width,
+            alignment,
+            lineSpacingMultiplier,
+            lineSpacingExtra,
+            metrics,
+            includePadding,
+            ellipsize,
+            ellipsizedWidth
+        )
+    }
 }
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
index ef17dfb..e2c9747 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
 import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
 import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
+import androidx.core.os.BuildCompat
 import java.lang.reflect.Constructor
 import java.lang.reflect.InvocationTargetException
 
@@ -41,7 +42,7 @@
     private val delegate: StaticLayoutFactoryImpl = if (Build.VERSION.SDK_INT >= 23) {
         StaticLayoutFactory23()
     } else {
-        StaticLayoutFactoryPre21()
+        StaticLayoutFactoryDefault()
     }
 
     /**
@@ -98,6 +99,20 @@
             )
         )
     }
+
+    /**
+     * Returns whether fallbackLineSpacing is enabled for the given layout.
+     *
+     * @param layout StaticLayout instance
+     * @param useFallbackLineSpacing fallbackLineSpacing canfiguration passed while creating the
+     * StaticLayout.
+     */
+    fun isFallbackLineSpacingEnabled(
+        layout: StaticLayout,
+        useFallbackLineSpacing: Boolean
+    ): Boolean {
+        return delegate.isFallbackLineSpacingEnabled(layout, useFallbackLineSpacing)
+    }
 }
 
 @OptIn(InternalPlatformTextApi::class)
@@ -136,6 +151,8 @@
 
     @DoNotInline // API level specific, do not inline to prevent ART class verification breakages
     fun create(params: StaticLayoutParams): StaticLayout
+
+    fun isFallbackLineSpacingEnabled(layout: StaticLayout, useFallbackLineSpacing: Boolean): Boolean
 }
 
 @RequiresApi(23)
@@ -166,10 +183,25 @@
                 }
             }.build()
     }
+
+    @androidx.annotation.OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
+    override fun isFallbackLineSpacingEnabled(
+        layout: StaticLayout,
+        useFallbackLineSpacing: Boolean
+    ): Boolean {
+        return if (BuildCompat.isAtLeastT()) {
+            StaticLayoutFactory33.isFallbackLineSpacingEnabled(layout)
+        } else if (Build.VERSION.SDK_INT >= 28) {
+            useFallbackLineSpacing
+        } else {
+            false
+        }
+    }
 }
 
 @RequiresApi(26)
 private object StaticLayoutFactory26 {
+    @JvmStatic
     @DoNotInline
     fun setJustificationMode(builder: Builder, justificationMode: Int) {
         builder.setJustificationMode(justificationMode)
@@ -178,13 +210,23 @@
 
 @RequiresApi(28)
 private object StaticLayoutFactory28 {
+    @JvmStatic
     @DoNotInline
     fun setUseLineSpacingFromFallbacks(builder: Builder, useFallbackLineSpacing: Boolean) {
         builder.setUseLineSpacingFromFallbacks(useFallbackLineSpacing)
     }
 }
 
-private class StaticLayoutFactoryPre21 : StaticLayoutFactoryImpl {
+@RequiresApi(33)
+private object StaticLayoutFactory33 {
+    @JvmStatic
+    @DoNotInline
+    fun isFallbackLineSpacingEnabled(layout: StaticLayout): Boolean {
+        return layout.isFallbackLineSpacingEnabled
+    }
+}
+
+private class StaticLayoutFactoryDefault : StaticLayoutFactoryImpl {
 
     companion object {
         private var isInitialized = false
@@ -274,4 +316,11 @@
             params.ellipsizedWidth
         )
     }
+
+    override fun isFallbackLineSpacingEnabled(
+        layout: StaticLayout,
+        useFallbackLineSpacing: Boolean
+    ): Boolean {
+        return false
+    }
 }
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 938b04b..ad49528 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -17,9 +17,10 @@
 
 import android.graphics.Canvas
 import android.graphics.Path
-import android.os.Build
+import android.text.BoringLayout
 import android.text.Layout
 import android.text.Spanned
+import android.text.StaticLayout
 import android.text.TextDirectionHeuristic
 import android.text.TextDirectionHeuristics
 import android.text.TextPaint
@@ -68,7 +69,11 @@
  * @param lineSpacingMultiplier the multiplier to be applied to each line of the text.
  * @param lineSpacingExtra the extra height to be added to each line of the text.
  * @param includePadding defines whether the extra space to be applied beyond font ascent and
- * descent,
+ * descent
+ * @param fallbackLineSpacing Sets Android TextView#setFallbackLineSpacing. This value should
+ * be set to true in most cases and it is the default on platform; otherwise tall scripts such
+ * as Burmese or Tibetan result in clippings on top and bottom sometimes making the text
+ * not-readable.
  * @param maxLines the maximum number of lines to be laid out.
  * @param breakStrategy the strategy to be used for line breaking
  * @param hyphenationFrequency set the frequency to control the amount of automatic hyphenation
@@ -81,7 +86,9 @@
  * element in the array is applied to the corresponding line. For lines past the last element in
  * array, the last element repeats.
  * @param layoutIntrinsics previously calculated [LayoutIntrinsics] for this text
+ *
  * @see StaticLayoutFactory
+ * @see BoringLayoutFactory
  *
  * @suppress
  */
@@ -174,6 +181,7 @@
                 metrics = boringMetrics,
                 alignment = frameworkAlignment,
                 includePadding = includePadding,
+                useFallbackLineSpacing = fallbackLineSpacing,
                 ellipsize = ellipsize,
                 ellipsizedWidth = widthInt
             )
@@ -439,7 +447,14 @@
     }
 
     internal fun isFallbackLinespacingApplied(): Boolean {
-        return fallbackLineSpacing && !isBoringLayout && Build.VERSION.SDK_INT >= 28
+        return if (isBoringLayout) {
+            BoringLayoutFactory.isFallbackLineSpacingEnabled(layout as BoringLayout)
+        } else {
+            StaticLayoutFactory.isFallbackLineSpacingEnabled(
+                layout as StaticLayout,
+                fallbackLineSpacing
+            )
+        }
     }
 }
 
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index 0fc43fc..5d2cdbb 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -1114,6 +1114,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     public fun launchComplicationDataSourceChooser() {
         ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 70046c3..a6b65d1 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -276,6 +276,7 @@
         }
 
         // Used by tests.
+        @Suppress("DEPRECATION")
         @Throws(TimeoutCancellationException::class)
         internal suspend fun createOnWatchEditorSessionImpl(
             activity: ComponentActivity,
@@ -1072,6 +1073,7 @@
         return intent
     }
 
+    @Suppress("DEPRECATION")
     override fun parseResult(resultCode: Int, intent: Intent?) = intent?.let {
         val extras = intent.extras?.let { extras ->
             Bundle(extras).apply { remove(EXTRA_PROVIDER_INFO) }
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index a266048..4ece658 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -138,6 +138,7 @@
          * if there is one or `null` otherwise. Intended for use by the watch face editor activity.
          * @throws [TimeoutCancellationException] in case of en error.
          */
+        @Suppress("DEPRECATION")
         @SuppressLint("NewApi")
         @JvmStatic
         @Throws(TimeoutCancellationException::class)
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
index 2294c89..f3218ce 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/ComplicationHelperActivityTest.kt
@@ -314,6 +314,7 @@
 }
 
 /** The watch face component name encoded in the intent. */
+@Suppress("DEPRECATION")
 private val Intent.watchFaceComponentName
     get() = getParcelableExtra<ComponentName>(
         ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
index b5853f9..0cac46c 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationHelperActivity.java
@@ -183,6 +183,7 @@
         }
 
         @Override
+        @SuppressWarnings("deprecation")
         public void launchComplicationDeniedActivity() {
             Intent complicationDeniedIntent =
                     mActivity.getIntent().getParcelableExtra(
@@ -222,6 +223,7 @@
         start(true);
     }
 
+    @SuppressWarnings("deprecation")
     void start(boolean shouldShowRequestPermissionRationale) {
         if (shouldShowRequestPermissionRationale
                 && mDelegate.shouldShowRequestPermissionRationale()) {
diff --git a/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
index 4d8d6fc..87ded0e 100644
--- a/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
+++ b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
@@ -100,6 +100,7 @@
          * @return The array of [RemoteInput] previously added with [putRemoteInputsExtra] or null
          * which means no user input required.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         @Nullable
         public fun getRemoteInputsExtra(intent: Intent): List<RemoteInput>? =
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
index 47c4577..3c9f8ba 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
@@ -97,6 +97,7 @@
          * @param intent The intent holding configuration.
          * @return The remote intent, or null if none was set.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         public fun getTargetIntent(intent: Intent): Intent? =
             intent.getParcelableExtra(EXTRA_INTENT)
@@ -117,6 +118,7 @@
          * @param intent The intent holding configuration.
          * @return The result receiver, or null if none was set.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         internal fun getRemoteIntentResultReceiver(intent: Intent): ResultReceiver? =
             intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
index 99aa404..54eefab 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
@@ -85,6 +85,7 @@
          * @return the value of an item previously added with [putWatchFaceComponentExtra], or
          * null if no value was found.
          */
+        @Suppress("DEPRECATION")
         @JvmStatic
         @Nullable
         public fun getWatchFaceComponentExtra(watchFaceIntent: Intent): ComponentName? =
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6a28e00..ad88a4a 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -7,6 +7,7 @@
   public interface WindowExtensions {
     method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
+    method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
 
@@ -16,6 +17,22 @@
 
 }
 
+package androidx.window.extensions.area {
+
+  @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface WindowAreaComponent {
+    method public void addRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void endRearDisplaySession();
+    method public void removeRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void startRearDisplaySession(android.app.Activity, java.util.function.Consumer<java.lang.Integer!>);
+    field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+    field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+    field public static final int STATUS_AVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.embedding {
 
   @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface ActivityEmbeddingComponent {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 6ac25bd..ca3a882 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -17,6 +17,7 @@
 package androidx.window.extensions;
 
 import androidx.annotation.Nullable;
+import androidx.window.extensions.area.WindowAreaComponent;
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
 import androidx.window.extensions.layout.WindowLayoutComponent;
 
@@ -60,4 +61,14 @@
     @Nullable
     @ExperimentalWindowExtensionsApi
     ActivityEmbeddingComponent getActivityEmbeddingComponent();
+
+    /**
+     * Returns the OEM implementation of {@link WindowAreaComponent} if it is supported on
+     * the device, {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions}.
+     * @return the OEM implementation of {@link WindowAreaComponent}
+     */
+    @Nullable
+    @ExperimentalWindowExtensionsApi
+    WindowAreaComponent getWindowAreaComponent();
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
new file mode 100644
index 0000000..a565eda
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.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.window.extensions.area;
+
+import android.app.Activity;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.ExperimentalWindowExtensionsApi;
+import androidx.window.extensions.WindowExtensions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Consumer;
+
+/**
+ * The interface definition that will be used by the WindowManager library to get custom
+ * OEM-provided behavior around moving windows between displays or display areas on a device.
+ *
+ * Currently the only behavior supported is RearDisplay Mode, where the window
+ * is moved to the display that faces the same direction as the rear camera.
+ *
+ * <p>This interface should be implemented by OEM and deployed to the target devices.
+ * @see WindowExtensions#getWindowLayoutComponent()
+ */
+@ExperimentalWindowExtensionsApi
+public interface WindowAreaComponent {
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * unsupported on this device. Could be due to the device not supporting that
+     * specific feature.
+     */
+    int STATUS_UNSUPPORTED = 0;
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * currently unavailable but is supported on this device. This value could signify
+     * that the current device state does not support the specific feature or another
+     * process is currently enabled in that feature.
+     */
+    int STATUS_UNAVAILABLE = 1;
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * available to be entered or enabled.
+     */
+    int STATUS_AVAILABLE = 2;
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            STATUS_UNSUPPORTED,
+            STATUS_UNAVAILABLE,
+            STATUS_AVAILABLE
+    })
+    @interface WindowAreaStatus {}
+
+    /**
+     * Session state constant to represent there being no active session
+     * currently in progress. Used by the library to call the correct callbacks if
+     * a session is ended.
+     */
+    int SESSION_STATE_INACTIVE = 0;
+
+    /**
+     * Session state constant to represent that there is an
+     * active session currently in progress. Used by the library to
+     * know when to return the session object to the developer when the
+     * session is created and active.
+     */
+    int SESSION_STATE_ACTIVE = 1;
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            SESSION_STATE_ACTIVE,
+            SESSION_STATE_INACTIVE
+    })
+    @interface WindowAreaSessionState {}
+
+    /**
+     * Adds a listener interested in receiving updates on the RearDisplayStatus
+     * of the device. Because this is being called from the OEM provided
+     * extensions, the library will post the result of the listener on the executor
+     * provided by the developer.
+     *
+     * The listener provided will receive values that
+     * correspond to the [WindowAreaStatus] value that aligns with the current status
+     * of the rear display.
+     * @param consumer interested in receiving updates to WindowAreaStatus.
+     */
+    void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /**
+     * Removes a listener no longer interested in receiving updates.
+     * @param consumer no longer interested in receiving updates to WindowAreaStatus
+     */
+    void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+
+    /**
+     * Creates and starts a rear display session and sends state updates to the
+     * consumer provided. This consumer will receive a constant represented by
+     * [WindowAreaSessionState] to represent the state of the current rear display
+     * session. We will translate to a more friendly interface in the library.
+     *
+     * Because this is being called from the OEM provided extensions, the library
+     * will post the result of the listener on the executor provided by the developer.
+     *
+     * @param activity to allow that the OEM implementation will use as a base
+     * context and to identify the source display area of the request.
+     * The reference to the activity instance must not be stored in the OEM
+     * implementation to prevent memory leaks.
+     * @param consumer to provide updates to the client on the status of the session
+     * @throws UnsupportedOperationException if this method is called when RearDisplay
+     * mode is not available. This could be to an incompatible device state or when
+     * another process is currently in this mode.
+     */
+    @SuppressWarnings("ExecutorRegistration") // Jetpack will post it on the app-provided executor.
+    void startRearDisplaySession(@NonNull Activity activity,
+            @NonNull Consumer<Integer> consumer);
+
+    /**
+     * Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
+     * provided in the {@code startRearDisplaySession} method. This method is only
+     * called through the {@code RearDisplaySession} provided to the developer.
+     */
+    void endRearDisplaySession();
+}
diff --git a/window/window-java/api/public_plus_experimental_current.txt b/window/window-java/api/public_plus_experimental_current.txt
index 709904b..a56988b 100644
--- a/window/window-java/api/public_plus_experimental_current.txt
+++ b/window/window-java/api/public_plus_experimental_current.txt
@@ -1,4 +1,15 @@
 // Signature format: 4.0
+package androidx.window.java.area {
+
+  @androidx.window.core.ExperimentalWindowApi public final class WindowAreaControllerJavaAdapter implements androidx.window.area.WindowAreaController {
+    ctor public WindowAreaControllerJavaAdapter(androidx.window.area.WindowAreaController controller);
+    method public void addRearDisplayStatusListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+    method public void removeRearDisplayStatusListener(androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+    method public void startRearDisplayModeSession(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+  }
+
+}
+
 package androidx.window.java.layout {
 
   public final class WindowInfoTrackerCallbackAdapter implements androidx.window.layout.WindowInfoTracker {
diff --git a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
new file mode 100644
index 0000000..584445f
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
@@ -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.window.java.area
+
+import android.app.Activity
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaStatus
+import androidx.window.area.WindowAreaController
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * An adapted interface for [WindowAreaController] that provides the information and
+ * functionality around RearDisplay Mode via a callback shaped API.
+ */
+@ExperimentalWindowApi
+class WindowAreaControllerJavaAdapter(
+    private val controller: WindowAreaController
+) : WindowAreaController by controller {
+
+    /**
+     * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
+     */
+    private val lock = ReentrantLock()
+    private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
+
+    /**
+     * Registers a listener to consume [WindowAreaStatus] values defined as
+     * [WindowAreaStatus.UNSUPPORTED], [WindowAreaStatus.UNAVAILABLE], and
+     * [WindowAreaStatus.AVAILABLE]. The values provided through this listener should be used
+     * to determine if you are able to enable rear display Mode at that time. You can use these
+     * values to modify your UI to show/hide controls and determine when to enable features
+     * that use rear display Mode. You should only try and enter rear display mode when your
+     * [consumer] is provided a value of [WindowAreaStatus.AVAILABLE].
+     *
+     * The [consumer] will be provided an initial value on registration, as well as any change
+     * to the status as they occur. This could happen due to hardware device state changes, or if
+     * another process has enabled RearDisplay Mode.
+     *
+     * @see WindowAreaController.rearDisplayStatus
+     */
+    fun addRearDisplayStatusListener(
+        executor: Executor,
+        consumer: Consumer<WindowAreaStatus>
+    ) {
+        val statusFlow = controller.rearDisplayStatus()
+        lock.withLock {
+            if (consumerToJobMap[consumer] == null) {
+                val scope = CoroutineScope(executor.asCoroutineDispatcher())
+                consumerToJobMap[consumer] = scope.launch {
+                    statusFlow.collect { consumer.accept(it) }
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes a listener of [WindowAreaStatus] values
+     * @see WindowAreaController.rearDisplayStatus
+     */
+    fun removeRearDisplayStatusListener(consumer: Consumer<WindowAreaStatus>) {
+        lock.withLock {
+            consumerToJobMap[consumer]?.cancel()
+            consumerToJobMap.remove(consumer)
+        }
+    }
+
+    /**
+     * Starts a RearDisplay Mode session and provides updates through the
+     * [WindowAreaSessionCallback] provided. Due to the nature of moving your Activity to a
+     * different display, your Activity will likely go through a configuration change. Because of
+     * this, if your Activity does not override configuration changes, this method should be called
+     * from a component that outlives the Activity lifecycle such as a
+     * [androidx.lifecycle.ViewModel]. If your Activity does override
+     * configuration changes, it is safe to call this method inside your Activity.
+     *
+     * This method should only be called if you have received a [WindowAreaStatus.AVAILABLE]
+     * value from the listener provided through the [addRearDisplayStatusListener] method. If
+     * you try and enable RearDisplay mode without it being available, you will receive an
+     * [UnsupportedOperationException].
+     *
+     * The [windowAreaSessionCallback] provided will receive a call to
+     * [WindowAreaSessionCallback.onSessionStarted] after your Activity has been moved to the
+     * display corresponding to this mode. RearDisplay mode will stay active until the session
+     * provided through [WindowAreaSessionCallback.onSessionStarted] is closed, or there is a device
+     * state change that makes RearDisplay mode incompatible such as if the device is closed so the
+     * outer-display is no longer in line with the rear camera. When this occurs,
+     * [WindowAreaSessionCallback.onSessionEnded] is called to notify you the session has been
+     * ended.
+     *
+     * @see addRearDisplayStatusListener
+     * @throws UnsupportedOperationException if you try and start a RearDisplay session when
+     * your [WindowAreaController.rearDisplayStatus] does not return a value of
+     * [WindowAreaStatus.AVAILABLE]
+     */
+    fun startRearDisplayModeSession(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        controller.rearDisplayMode(activity, executor, windowAreaSessionCallback)
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/build.gradle b/window/window-samples/build.gradle
index 1b3060c1..e66802d 100644
--- a/window/window-samples/build.gradle
+++ b/window/window-samples/build.gradle
@@ -46,7 +46,7 @@
     implementation "androidx.browser:browser:1.3.0"
     implementation("androidx.startup:startup-runtime:1.1.0")
 
-    implementation(project(":window:window"))
+    implementation(project(":window:window-java"))
     debugImplementation(libs.leakcanary)
 
     androidTestImplementation(libs.testCore)
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index de10c6a..76ad8e2 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -47,6 +47,16 @@
             android:exported="false"
             android:configChanges="orientation|screenSize|screenLayout|screenSize"
             android:label="@string/window_metrics"/>
+        <activity android:name=".RearDisplayActivityConfigChanges"
+            android:exported="true"
+            android:configChanges=
+                "orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:label="@string/rear_display">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
         <activity
             android:name=".embedding.SplitActivityA"
             android:exported="true"
diff --git a/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt b/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt
new file mode 100644
index 0000000..f7e839f
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/RearDisplayActivityConfigChanges.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.window.sample
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaSession
+import androidx.window.area.WindowAreaStatus
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.java.area.WindowAreaControllerJavaAdapter
+import androidx.window.sample.databinding.ActivityRearDisplayBinding
+import androidx.window.sample.infolog.InfoLogAdapter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.Executor
+
+/**
+ * Demo Activity that showcases listening for RearDisplay Status
+ * as well as enabling/disabling RearDisplay mode. This Activity
+ * implements [WindowAreaSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
+
+    private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
+    private var rearDisplaySession: WindowAreaSession? = null
+    private val infoLogAdapter = InfoLogAdapter()
+    private lateinit var binding: ActivityRearDisplayBinding
+    private lateinit var executor: Executor
+
+    private val rearDisplayStatusListener = Consumer<WindowAreaStatus> { status ->
+        infoLogAdapter.append(getCurrentTimeString(), status.toString())
+        infoLogAdapter.notifyDataSetChanged()
+        updateRearDisplayButton(status)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityRearDisplayBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        executor = ContextCompat.getMainExecutor(this)
+        windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())
+
+        binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+        binding.rearDisplayButton.setOnClickListener {
+            if (rearDisplaySession != null) {
+                rearDisplaySession?.close()
+            } else {
+                windowAreaController.startRearDisplayModeSession(
+                    this,
+                    executor,
+                    this)
+            }
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        windowAreaController.addRearDisplayStatusListener(
+            executor,
+            rearDisplayStatusListener
+        )
+    }
+
+    override fun onStop() {
+        super.onStop()
+        windowAreaController.removeRearDisplayStatusListener(rearDisplayStatusListener)
+    }
+
+    override fun onSessionStarted(session: WindowAreaSession) {
+        rearDisplaySession = session
+        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
+        infoLogAdapter.notifyDataSetChanged()
+    }
+
+    override fun onSessionEnded() {
+        rearDisplaySession = null
+        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
+        infoLogAdapter.notifyDataSetChanged()
+    }
+
+    private fun updateRearDisplayButton(status: WindowAreaStatus) {
+        if (rearDisplaySession != null) {
+            binding.rearDisplayButton.isEnabled = true
+            binding.rearDisplayButton.text = "Disable RearDisplay Mode"
+            return
+        }
+        when (status) {
+            WindowAreaStatus.UNSUPPORTED -> {
+                binding.rearDisplayButton.isEnabled = false
+                binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+            }
+            WindowAreaStatus.UNAVAILABLE -> {
+                binding.rearDisplayButton.isEnabled = false
+                binding.rearDisplayButton.text = "RearDisplay is not currently available"
+            }
+            WindowAreaStatus.AVAILABLE -> {
+                binding.rearDisplayButton.isEnabled = true
+                binding.rearDisplayButton.text = "Enable RearDisplay Mode"
+            }
+        }
+    }
+
+    private fun getCurrentTimeString(): String {
+        val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+        val currentDate = sdf.format(Date())
+        return currentDate.toString()
+    }
+
+    private companion object {
+        private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_rear_display.xml b/window/window-samples/src/main/res/layout/activity_rear_display.xml
new file mode 100644
index 0000000..43bea60
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/activity_rear_display.xml
@@ -0,0 +1,42 @@
+<?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.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/rearStatusRecyclerView"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <Button
+        android:id="@+id/rear_display_button"
+        android:text="Enable RearDisplay"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAlignment="center"
+        android:layout_marginBottom="32dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index 5faaae6..25d5eef 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -51,4 +51,6 @@
     <string name="occlusion_is_none">Occlusion is none</string>
     <string name="window_metrics">Window metrics</string>
     <string name="window_metrics_description">Demo of using WindowMetrics API with activity handling rotations.</string>
+    <string name="rear_display">Rear Display Mode</string>
+    <string name="rear_display_description">Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode</string>
 </resources>
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index e166e4b..3f1b25c 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -1,4 +1,36 @@
 // Signature format: 4.0
+package androidx.window.area {
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaController {
+    method public default static androidx.window.area.WindowAreaController getOrCreate();
+    field public static final androidx.window.area.WindowAreaController.Companion Companion;
+  }
+
+  public static final class WindowAreaController.Companion {
+    method public androidx.window.area.WindowAreaController getOrCreate();
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSession {
+    method public void close();
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionCallback {
+    method public void onSessionEnded();
+    method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public final class WindowAreaStatus {
+    field public static final androidx.window.area.WindowAreaStatus AVAILABLE;
+    field public static final androidx.window.area.WindowAreaStatus.Companion Companion;
+    field public static final androidx.window.area.WindowAreaStatus UNAVAILABLE;
+    field public static final androidx.window.area.WindowAreaStatus UNSUPPORTED;
+  }
+
+  public static final class WindowAreaStatus.Companion {
+  }
+
+}
+
 package androidx.window.core {
 
   @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWindowApi {
diff --git a/window/window/build.gradle b/window/window/build.gradle
index ca84543..98b94b3 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -64,6 +64,7 @@
     testImplementation(compileOnly(project(":window:extensions:extensions")))
 
     androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.kotlinTestJunit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
new file mode 100644
index 0000000..dad5e14
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.window.area
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.window.TestActivity
+import androidx.window.TestConsumer
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import java.util.function.Consumer
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Ignore
+
+@Ignore // todo(b/222407443)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+class WindowAreaControllerImplTest {
+
+    @get:Rule
+    public val activityScenario: ActivityScenarioRule<TestActivity> =
+        ActivityScenarioRule(TestActivity::class.java)
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayStatus(): Unit = testScope.runTest {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+        activityScenario.scenario.onActivity {
+            val extensionComponent = FakeWindowAreaComponent()
+            val repo = WindowAreaControllerImpl(extensionComponent)
+            val collector = TestConsumer<WindowAreaStatus>()
+            extensionComponent
+                .updateStatusListeners(WindowAreaComponent.STATUS_UNAVAILABLE)
+            testScope.launch {
+                repo.rearDisplayStatus().collect(collector::accept)
+            }
+            collector.assertValue(WindowAreaStatus.UNAVAILABLE)
+            extensionComponent
+                .updateStatusListeners(WindowAreaComponent.STATUS_AVAILABLE)
+            collector.assertValues(
+                WindowAreaStatus.UNAVAILABLE,
+                WindowAreaStatus.AVAILABLE
+            )
+        }
+    }
+
+    @Test
+    public fun testRearDisplayStatusNullComponent(): Unit = testScope.runTest {
+        activityScenario.scenario.onActivity {
+            val repo = EmptyWindowAreaControllerImpl()
+            val collector = TestConsumer<WindowAreaStatus>()
+            testScope.launch {
+                repo.rearDisplayStatus().collect(collector::accept)
+            }
+            collector.assertValue(WindowAreaStatus.UNSUPPORTED)
+        }
+    }
+
+    /**
+     * Tests the rear display mode flow works as expected. Tests the flow
+     * through WindowAreaControllerImpl with a fake extension. This fake extension
+     * changes the orientation of the activity to landscape when rear display mode is enabled
+     * and then returns it back to portrait when it's disabled.
+     */
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayMode(): Unit = testScope.runTest {
+        val extensions = FakeWindowAreaComponent()
+        val repo = WindowAreaControllerImpl(extensions)
+        extensions.currentStatus = WindowAreaComponent.STATUS_AVAILABLE
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            testActivity.resetLayoutCounter()
+            testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            testActivity.waitForLayout()
+        }
+
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+            testActivity.resetLayoutCounter()
+            repo.rearDisplayMode(testActivity, Runnable::run, callback)
+        }
+
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+            assert(callback.currentSession != null)
+            testActivity.resetLayoutCounter()
+            callback.endSession()
+        }
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+            assert(callback.currentSession == null)
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayModeReturnsError(): Unit = testScope.runTest {
+        val extensionComponent = FakeWindowAreaComponent()
+        extensionComponent.currentStatus = WindowAreaComponent.STATUS_UNAVAILABLE
+        val repo = WindowAreaControllerImpl(extensionComponent)
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            assertFailsWith(
+                exceptionClass = UnsupportedOperationException::class,
+                block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+            )
+        }
+    }
+
+    @Test
+    public fun testRearDisplayModeNullComponent(): Unit = testScope.runTest {
+        val repo = EmptyWindowAreaControllerImpl()
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            assertFailsWith(
+                exceptionClass = UnsupportedOperationException::class,
+                block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+            )
+        }
+    }
+
+    private class FakeWindowAreaComponent : WindowAreaComponent {
+        val statusListeners = mutableListOf<Consumer<Int>>()
+        var currentStatus = WindowAreaComponent.STATUS_UNSUPPORTED
+        var testActivity: Activity? = null
+        var sessionConsumer: Consumer<Int>? = null
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            statusListeners.add(consumer)
+            consumer.accept(currentStatus)
+        }
+
+        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            statusListeners.remove(consumer)
+        }
+
+        // Fake WindowAreaComponent will change the orientation of the activity to signal
+        // entering rear display mode, as well as ending the session
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun startRearDisplaySession(
+            activity: Activity,
+            rearDisplaySessionConsumer: Consumer<Int>
+        ) {
+            if (currentStatus != WindowAreaComponent.STATUS_AVAILABLE) {
+                throw WindowAreaController.REAR_DISPLAY_ERROR
+            }
+            testActivity = activity
+            sessionConsumer = rearDisplaySessionConsumer
+            testActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+            rearDisplaySessionConsumer.accept(WindowAreaComponent.SESSION_STATE_ACTIVE)
+        }
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun endRearDisplaySession() {
+            testActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            sessionConsumer?.accept(WindowAreaComponent.SESSION_STATE_INACTIVE)
+        }
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        fun updateStatusListeners(newStatus: Int) {
+            currentStatus = newStatus
+            for (consumer in statusListeners) {
+                consumer.accept(currentStatus)
+            }
+        }
+    }
+
+    private class TestWindowAreaSessionCallback : WindowAreaSessionCallback {
+
+        var currentSession: WindowAreaSession? = null
+        var error: Throwable? = null
+
+        override fun onSessionStarted(session: WindowAreaSession) {
+            currentSession = session
+        }
+
+        override fun onSessionEnded() {
+            currentSession = null
+        }
+
+        fun endSession() = currentSession?.close()
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
new file mode 100644
index 0000000..996c7ad
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
@@ -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.window.area
+
+import android.app.Activity
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import java.util.concurrent.Executor
+
+/**
+ * Empty Implementation for devices that do not
+ * support the [WindowAreaController] functionality
+ */
+@ExperimentalWindowApi
+internal class EmptyWindowAreaControllerImpl : WindowAreaController {
+    override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+        return flowOf(WindowAreaStatus.UNSUPPORTED)
+    }
+
+    override fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        throw WindowAreaController.REAR_DISPLAY_ERROR
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
new file mode 100644
index 0000000..9a5bbd3
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
@@ -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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+@ExperimentalWindowApi
+internal class RearDisplaySessionImpl(
+    private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaSession {
+
+    override fun close() {
+        windowAreaComponent.endRearDisplaySession()
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
new file mode 100644
index 0000000..05aa96e
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.window.core.BuildConfig
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.SpecificationComputer
+import androidx.window.extensions.WindowExtensionsProvider
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.flow.Flow
+import java.util.concurrent.Executor
+import kotlin.jvm.Throws
+
+/**
+ * An interface to provide the information and behavior around moving windows between
+ * displays or display areas on a device.
+ */
+@ExperimentalWindowApi
+interface WindowAreaController {
+
+    /*
+    Marked with RestrictTo as we iterate and define the
+    Kotlin API we want to provide.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun rearDisplayStatus(): Flow<WindowAreaStatus>
+
+    /*
+    Marked with RestrictTo as we iterate and define the
+    Kotlin API we want to provide.
+     */
+    @Throws(UnsupportedOperationException::class)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    )
+
+    public companion object {
+        internal val REAR_DISPLAY_ERROR =
+            UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+
+        private val TAG = WindowAreaController::class.simpleName
+
+        private var decorator: WindowAreaControllerDecorator = EmptyDecorator
+
+        /**
+         * Provides an instance of [WindowAreaController].
+         */
+        @JvmName("getOrCreate")
+        @JvmStatic
+        public fun getOrCreate(): WindowAreaController {
+            var windowAreaComponentExtensions: WindowAreaComponent?
+            try {
+                windowAreaComponentExtensions = WindowExtensionsProvider
+                    .getWindowExtensions()
+                    .windowAreaComponent
+            } catch (t: Throwable) {
+                if (BuildConfig.verificationMode == SpecificationComputer.VerificationMode.STRICT) {
+                    Log.d(TAG, "Failed to load WindowExtensions")
+                }
+                windowAreaComponentExtensions = null
+            }
+            val controller =
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+                    windowAreaComponentExtensions == null) {
+                    EmptyWindowAreaControllerImpl()
+                } else {
+                    WindowAreaControllerImpl(windowAreaComponentExtensions)
+                }
+            return decorator.decorate(controller)
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public fun overrideDecorator(overridingDecorator: WindowAreaControllerDecorator) {
+            decorator = overridingDecorator
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public fun reset() {
+            decorator = EmptyDecorator
+        }
+    }
+}
+
+/**
+ * Decorator that allows us to provide different functionality
+ * in our window-testing artifact.
+ */
+@ExperimentalWindowApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface WindowAreaControllerDecorator {
+    /**
+     * Returns an instance of [WindowAreaController] associated to the [Activity]
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun decorate(controller: WindowAreaController): WindowAreaController
+}
+
+@ExperimentalWindowApi
+private object EmptyDecorator : WindowAreaControllerDecorator {
+    override fun decorate(controller: WindowAreaController): WindowAreaController {
+        return controller
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
new file mode 100644
index 0000000..0e156818
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.window.core.BuildConfig
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.SpecificationComputer
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+/**
+ * Implementation of WindowAreaController for devices
+ * that do implement the WindowAreaComponent on device.
+ *
+ * Requires [Build.VERSION_CODES.N] due to the use of [Consumer].
+ * Will not be created though on API levels lower than
+ * [Build.VERSION_CODES.S] as that's the min level of support for
+ * this functionality.
+ */
+@ExperimentalWindowApi
+@RequiresApi(Build.VERSION_CODES.N)
+internal class WindowAreaControllerImpl(
+    private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaController {
+
+    private lateinit var rearDisplaySessionConsumer: Consumer<Int>
+    private var currentStatus: WindowAreaStatus? = null
+
+    override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+        return flow {
+            val channel = Channel<WindowAreaStatus>(
+                capacity = BUFFER_CAPACITY,
+                onBufferOverflow = BufferOverflow.DROP_OLDEST
+            )
+            val listener = Consumer<Int> { status ->
+                currentStatus = WindowAreaStatus.translate(status)
+                channel.trySend(currentStatus ?: WindowAreaStatus.UNSUPPORTED)
+            }
+            windowAreaComponent.addRearDisplayStatusListener(listener)
+            try {
+                for (item in channel) {
+                    emit(item)
+                }
+            } finally {
+                windowAreaComponent.removeRearDisplayStatusListener(listener)
+            }
+        }.distinctUntilChanged()
+    }
+
+    override fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        // If we already have a status value that is not [WindowAreaStatus.AVAILABLE]
+        // we should throw an exception quick to indicate they tried to enable
+        // RearDisplay mode when it was not available.
+        if (currentStatus != null && currentStatus != WindowAreaStatus.AVAILABLE) {
+            throw WindowAreaController.REAR_DISPLAY_ERROR
+        }
+        rearDisplaySessionConsumer =
+            RearDisplaySessionConsumer(windowAreaSessionCallback, windowAreaComponent)
+        windowAreaComponent.startRearDisplaySession(activity, rearDisplaySessionConsumer)
+    }
+
+    internal class RearDisplaySessionConsumer(
+        private val appCallback: WindowAreaSessionCallback,
+        private val extensionsComponent: WindowAreaComponent
+    ) : Consumer<Int> {
+
+        private var session: WindowAreaSession? = null
+
+        override fun accept(sessionStatus: Int) {
+            when (sessionStatus) {
+                WindowAreaComponent.SESSION_STATE_ACTIVE -> onSessionStarted()
+                WindowAreaComponent.SESSION_STATE_INACTIVE -> onSessionFinished()
+                else -> {
+                    if (BuildConfig.verificationMode ==
+                            SpecificationComputer.VerificationMode.STRICT) {
+                        Log.d(TAG, "Received an unknown session status value: $sessionStatus")
+                    }
+                    onSessionFinished()
+                }
+            }
+        }
+
+        private fun onSessionStarted() {
+            session = RearDisplaySessionImpl(extensionsComponent)
+            session?.let { appCallback.onSessionStarted(it) }
+        }
+
+        private fun onSessionFinished() {
+            session = null
+            appCallback.onSessionEnded()
+        }
+    }
+
+    internal companion object {
+        private val TAG = WindowAreaControllerImpl::class.simpleName
+        /*
+        Chosen as 10 for a base default value. We shouldn't be receiving
+        many changes to window area status so this is enough capacity
+        to not end up blocking.
+         */
+        private const val BUFFER_CAPACITY = 10
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
new file mode 100644
index 0000000..6cdbd12
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+
+/**
+ * Session interface to represent a long-standing
+ * WindowArea mode or feature that provides a handle
+ * to close the session.
+ */
+@ExperimentalWindowApi
+public interface WindowAreaSession {
+    fun close()
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
new file mode 100644
index 0000000..80842c4
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
@@ -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.
+ */
+
+package androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+
+/** Callback to update the client on the WindowArea Session being
+ * started and ended.
+ * TODO(b/207720511) Move to window-java module when Kotlin API Finalized
+ */
+@ExperimentalWindowApi
+interface WindowAreaSessionCallback {
+
+    fun onSessionStarted(session: WindowAreaSession)
+
+    fun onSessionEnded()
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
new file mode 100644
index 0000000..f60d8f5
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
@@ -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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+/**
+ * Represents a window area status.
+ */
+@ExperimentalWindowApi
+class WindowAreaStatus private constructor(private val mDescription: String) {
+    override fun toString(): String {
+        return mDescription
+    }
+
+    companion object {
+        /**
+         * Status representing that the WindowArea feature is not a supported
+         * feature on the device.
+         */
+        @JvmField
+        val UNSUPPORTED = WindowAreaStatus("UNSUPPORTED")
+
+        /**
+         * Status representing that the WindowArea feature is currently not available
+         * to be enabled. This could be due to another process has enabled it, or that the
+         * current device configuration doesn't allow it.
+         */
+        @JvmField
+        val UNAVAILABLE = WindowAreaStatus("UNAVAILABLE")
+
+        /**
+         * Status representing that the WindowArea feature is available to be enabled.
+         */
+        @JvmField
+        val AVAILABLE = WindowAreaStatus("AVAILABLE")
+
+        @JvmStatic
+        internal fun translate(status: Int): WindowAreaStatus {
+            return when (status) {
+                WindowAreaComponent.STATUS_AVAILABLE -> AVAILABLE
+                WindowAreaComponent.STATUS_UNAVAILABLE -> UNAVAILABLE
+                else -> UNSUPPORTED
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
new file mode 100644
index 0000000..04d3702
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
@@ -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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import org.junit.Test
+
+/**
+ * Unit tests for [WindowAreaStatus] that run on the JVM.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class WindowAreaStatusUnitTest {
+
+    @Test
+    fun testWindowAreaStatusTranslateValueAvailable() {
+        val expected = WindowAreaStatus.AVAILABLE
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_AVAILABLE)
+        assert(expected == translateValue)
+    }
+
+    @Test
+    fun testWindowAreaStatusTranslateValueUnavailable() {
+        val expected = WindowAreaStatus.UNAVAILABLE
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNAVAILABLE)
+        assert(expected == translateValue)
+    }
+
+    @Test
+    fun testWindowAreaStatusTranslateValueUnsupported() {
+        val expected = WindowAreaStatus.UNSUPPORTED
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNSUPPORTED)
+        assert(expected == translateValue)
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime-ktx/api/2.7.0-beta01.txt b/work/work-runtime-ktx/api/2.7.0-beta01.txt
index 8535d65..2c5f419 100644
--- a/work/work-runtime-ktx/api/2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/2.7.0-beta01.txt
@@ -5,8 +5,6 @@
     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 suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
-    method 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/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt b/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
index 8535d65..2d666e3 100644
--- a/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/public_plus_experimental_2.7.0-beta01.txt
@@ -5,8 +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 public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
-    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    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/work-runtime-ktx/api/restricted_2.7.0-beta01.txt b/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
index 8535d65..2c5f419 100644
--- a/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
+++ b/work/work-runtime-ktx/api/restricted_2.7.0-beta01.txt
@@ -5,8 +5,6 @@
     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 suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> p);
-    method 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/work-runtime/api/2.7.0-beta01.txt b/work/work-runtime/api/2.7.0-beta01.txt
index a6de624..54713f5 100644
--- a/work/work-runtime/api/2.7.0-beta01.txt
+++ b/work/work-runtime/api/2.7.0-beta01.txt
@@ -154,7 +154,6 @@
   public abstract class ListenableWorker {
     ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public final android.content.Context getApplicationContext();
-    method 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();
@@ -216,11 +215,6 @@
   public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
   }
 
-  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!>);
@@ -347,7 +341,6 @@
     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 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/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt b/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt
index a6de624..0c2f419 100644
--- a/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.txt
+++ b/work/work-runtime/api/public_plus_experimental_2.7.0-beta01.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,7 +157,7 @@
   public abstract class ListenableWorker {
     ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public final android.content.Context getApplicationContext();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    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();
@@ -216,7 +219,7 @@
   public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
   }
 
-  public enum OutOfQuotaPolicy {
+  @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;
   }
@@ -347,7 +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 public B setExpedited(androidx.work.OutOfQuotaPolicy);
+    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/work-runtime/api/restricted_2.7.0-beta01.txt b/work/work-runtime/api/restricted_2.7.0-beta01.txt
index a6de624..54713f5 100644
--- a/work/work-runtime/api/restricted_2.7.0-beta01.txt
+++ b/work/work-runtime/api/restricted_2.7.0-beta01.txt
@@ -154,7 +154,6 @@
   public abstract class ListenableWorker {
     ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public final android.content.Context getApplicationContext();
-    method 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();
@@ -216,11 +215,6 @@
   public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
   }
 
-  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!>);
@@ -347,7 +341,6 @@
     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 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/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
index ae4eb5c..b956663 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/foreground/SystemForegroundDispatcher.java
@@ -253,6 +253,7 @@
         });
     }
 
+    @SuppressWarnings("deprecation")
     @MainThread
     private void handleNotify(@NonNull Intent intent) {
         int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);