Merge "Macro Benchmark for Semantics" into androidx-main am: 8c1fc2090d

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

Change-Id: If29f697772dbcdb9681819dc16edf63bc523f091
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 0d8a233..1d02f4a 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -63,5 +63,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.activity.compose"
 }
diff --git a/activity/activity-compose/integration-tests/activity-demos/build.gradle b/activity/activity-compose/integration-tests/activity-demos/build.gradle
index bfcfdbe..a541a1c 100644
--- a/activity/activity-compose/integration-tests/activity-demos/build.gradle
+++ b/activity/activity-compose/integration-tests/activity-demos/build.gradle
@@ -44,5 +44,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.activity.compose.demos"
 }
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 9f5fa5b..6086b6d 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -54,5 +54,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.activity.compose.samples"
 }
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 6cc0206..3d1ee71 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -66,5 +66,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.activity.ktx"
 }
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index e5aaa29..cbe8aa5 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -305,8 +305,16 @@
   }
 
   public final class PickVisualMediaRequest {
+    method public long getAccentColor();
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
     method public int getMaxItems();
     method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    method public boolean isCustomAccentColorApplied();
+    method public boolean isOrderedSelection();
+    property public final long accentColor;
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
+    property public final boolean isCustomAccentColorApplied;
+    property public final boolean isOrderedSelection;
     property public final int maxItems;
     property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
   }
@@ -314,13 +322,18 @@
   public static final class PickVisualMediaRequest.Builder {
     ctor public PickVisualMediaRequest.Builder();
     method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
     method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
     method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
   }
 
   public final class PickVisualMediaRequestKt {
     method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
-    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+    method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
   }
 
 }
@@ -415,6 +428,9 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
     field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
     field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
     field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
   }
 
@@ -423,6 +439,23 @@
     method public boolean isPhotoPickerAvailable(android.content.Context context);
   }
 
+  public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public abstract int getValue();
+    property public abstract int value;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public int getValue();
+    property public int value;
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public int getValue();
+    property public int value;
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
+  }
+
   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;
   }
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index fdca347..b405719 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -304,8 +304,16 @@
   }
 
   public final class PickVisualMediaRequest {
+    method public long getAccentColor();
+    method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab getDefaultTab();
     method public int getMaxItems();
     method public androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType getMediaType();
+    method public boolean isCustomAccentColorApplied();
+    method public boolean isOrderedSelection();
+    property public final long accentColor;
+    property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab;
+    property public final boolean isCustomAccentColorApplied;
+    property public final boolean isOrderedSelection;
     property public final int maxItems;
     property public final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType;
   }
@@ -313,13 +321,18 @@
   public static final class PickVisualMediaRequest.Builder {
     ctor public PickVisualMediaRequest.Builder();
     method public androidx.activity.result.PickVisualMediaRequest build();
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setAccentColor(long accentColor);
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setDefaultTab(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
     method public androidx.activity.result.PickVisualMediaRequest.Builder setMaxItems(@IntRange(from=2L) int maxItems);
     method public androidx.activity.result.PickVisualMediaRequest.Builder setMediaType(androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
+    method public androidx.activity.result.PickVisualMediaRequest.Builder setOrderedSelection(boolean isOrderedSelection);
   }
 
   public final class PickVisualMediaRequestKt {
     method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType);
-    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+    method @Deprecated public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems);
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
+    method public static androidx.activity.result.PickVisualMediaRequest PickVisualMediaRequest(long accentColor, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType mediaType, optional @IntRange(from=2L) int maxItems, optional boolean isOrderedSelection, optional androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab defaultTab);
   }
 
 }
@@ -414,6 +427,9 @@
     method public final android.net.Uri? parseResult(int resultCode, android.content.Intent? intent);
     field public static final String ACTION_SYSTEM_FALLBACK_PICK_IMAGES = "androidx.activity.result.contract.action.PICK_IMAGES";
     field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion Companion;
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR = "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR";
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER = "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER";
+    field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB = "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB";
     field public static final String EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX = "androidx.activity.result.contract.extra.PICK_IMAGES_MAX";
   }
 
@@ -422,6 +438,23 @@
     method public boolean isPhotoPickerAvailable(android.content.Context context);
   }
 
+  public abstract static class ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public abstract int getValue();
+    property public abstract int value;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public int getValue();
+    property public int value;
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab INSTANCE;
+  }
+
+  public static final class ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab extends androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab {
+    method public int getValue();
+    property public int value;
+    field public static final androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab INSTANCE;
+  }
+
   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;
   }
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 1cd39fb..c11bee0 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -15,6 +15,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.activity"
 }
 
diff --git a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
index 004ca84..d24f8b4 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
@@ -33,6 +33,7 @@
 @RunWith(AndroidJUnit4::class)
 class EdgeToEdgeTest {
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableAuto() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -73,6 +74,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableCustom() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -117,6 +119,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableDark() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -153,6 +156,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableLight() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
index b51e602..abe6540 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
@@ -109,4 +109,43 @@
         assertThat(intent.getIntExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, /* defaultValue= */ 0))
             .isEqualTo(/* expected= */ 7)
     }
+
+    @Test
+    fun testPickVisualMediaRequest_accentColor() {
+        // test default
+        var request = PickVisualMediaRequest()
+        assertThat(request.isCustomAccentColorApplied).isEqualTo(false)
+
+        // test given accent color in PickVisualMediaRequest
+        request = PickVisualMediaRequest(accentColor = 0xffff0000)
+        assertThat(request.isCustomAccentColorApplied).isEqualTo(true)
+        assertThat(request.accentColor).isEqualTo(0xffff0000)
+    }
+
+    @Test
+    fun testPickVisualMediaRequest_defaultTab() {
+        // test default
+        var request = PickVisualMediaRequest()
+        assertThat(request.defaultTab)
+            .isEqualTo(ActivityResultContracts.PickVisualMedia.DefaultTab.PhotosTab)
+
+        // test given default tab in PickVisualMediaRequest
+        request =
+            PickVisualMediaRequest(
+                defaultTab = ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab
+            )
+        assertThat(request.defaultTab)
+            .isEqualTo(ActivityResultContracts.PickVisualMedia.DefaultTab.AlbumsTab)
+    }
+
+    @Test
+    fun testPickVisualMediaRequest_isOrderedSelection() {
+        // test default
+        var request = PickVisualMediaRequest()
+        assertThat(request.isOrderedSelection).isEqualTo(false)
+
+        // test given isOrderedSelection in PickVisualMediaRequest
+        request = PickVisualMediaRequest(isOrderedSelection = true)
+        assertThat(request.isOrderedSelection).isEqualTo(true)
+    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
index 4f78044..73953fb 100644
--- a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
+++ b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
@@ -254,6 +254,7 @@
 @RequiresApi(23)
 private class EdgeToEdgeApi23 : EdgeToEdgeBase() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
@@ -273,6 +274,7 @@
 @RequiresApi(26)
 private open class EdgeToEdgeApi26 : EdgeToEdgeBase() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
@@ -305,6 +307,7 @@
 @RequiresApi(29)
 private open class EdgeToEdgeApi29 : EdgeToEdgeApi28() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
diff --git a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
index fa023fe..16569cd 100644
--- a/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/PickVisualMediaRequest.kt
@@ -17,6 +17,7 @@
 package androidx.activity.result
 
 import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.DefaultTab
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
 import androidx.annotation.IntRange
@@ -45,6 +46,10 @@
  * @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
  * @return a PickVisualMediaRequest that contains the given input
  */
+@Deprecated(
+    "Superseded by PickVisualMediaRequest that take optional isOrderedSelection and defaultTab",
+    level = DeprecationLevel.HIDDEN
+) // Binary API compatibility.
 @Suppress("MissingJvmstatic")
 fun PickVisualMediaRequest(
     mediaType: VisualMediaType = ImageAndVideo,
@@ -52,6 +57,61 @@
 ) = PickVisualMediaRequest.Builder().setMediaType(mediaType).setMaxItems(maxItems).build()
 
 /**
+ * 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
+ * @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
+ * @param isOrderedSelection whether the user can control the order of selected media when using
+ *   [PickMultipleVisualMedia] (defaults to false)
+ * @param defaultTab the tab to initially open in the picker (defaults to [DefaultTab.PhotosTab])
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+@Suppress("MissingJvmstatic")
+fun PickVisualMediaRequest(
+    mediaType: VisualMediaType = ImageAndVideo,
+    @IntRange(from = 2) maxItems: Int = PickMultipleVisualMedia.getMaxItems(),
+    isOrderedSelection: Boolean = false,
+    defaultTab: DefaultTab = DefaultTab.PhotosTab
+) =
+    PickVisualMediaRequest.Builder()
+        .setMediaType(mediaType)
+        .setMaxItems(maxItems)
+        .setOrderedSelection(isOrderedSelection)
+        .setDefaultTab(defaultTab)
+        .build()
+
+/**
+ * Creates a request for a
+ * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
+ * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
+ *
+ * @param accentColor color long to customize picker accent color
+ * @param mediaType type to go into the PickVisualMediaRequest
+ * @param maxItems limit the number of selectable items when using [PickMultipleVisualMedia]
+ * @param isOrderedSelection whether the user can control the order of selected media when using
+ *   [PickMultipleVisualMedia] (defaults to false)
+ * @param defaultTab the tab to initially open in the picker (defaults to [DefaultTab.PhotosTab])
+ * @return a PickVisualMediaRequest that contains the given input
+ */
+@Suppress("MissingJvmstatic")
+fun PickVisualMediaRequest(
+    accentColor: Long,
+    mediaType: VisualMediaType = ImageAndVideo,
+    @IntRange(from = 2) maxItems: Int = PickMultipleVisualMedia.getMaxItems(),
+    isOrderedSelection: Boolean = false,
+    defaultTab: DefaultTab = DefaultTab.PhotosTab
+) =
+    PickVisualMediaRequest.Builder()
+        .setMediaType(mediaType)
+        .setMaxItems(maxItems)
+        .setOrderedSelection(isOrderedSelection)
+        .setDefaultTab(defaultTab)
+        .setAccentColor(accentColor)
+        .build()
+
+/**
  * A request for a
  * [androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia] or
  * [androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia] Activity Contract.
@@ -64,11 +124,27 @@
     var maxItems: Int = PickMultipleVisualMedia.getMaxItems()
         internal set
 
+    var isOrderedSelection: Boolean = false
+        internal set
+
+    var defaultTab: DefaultTab = DefaultTab.PhotosTab
+        internal set
+
+    var isCustomAccentColorApplied: Boolean = false
+        internal set
+
+    var accentColor: Long = 0
+        internal set
+
     /** A builder for constructing [PickVisualMediaRequest] instances. */
     class Builder {
 
         private var mediaType: VisualMediaType = ImageAndVideo
         private var maxItems: Int = PickMultipleVisualMedia.getMaxItems()
+        private var isOrderedSelection: Boolean = false
+        private var defaultTab: DefaultTab = DefaultTab.PhotosTab
+        private var isCustomAccentColorApplied: Boolean = false
+        private var accentColor: Long = 0
 
         /**
          * Set the media type for the [PickVisualMediaRequest].
@@ -97,6 +173,50 @@
         }
 
         /**
+         * Set the ordered selection for the [PickVisualMediaRequest].
+         *
+         * Allow the user to control the order in which images are returned to the calling app. This
+         * parameter might be not supported by the underlying photo picker implementation.
+         *
+         * @param isOrderedSelection boolean to enable customisable selection order in the picker
+         * @return This builder.
+         */
+        fun setOrderedSelection(isOrderedSelection: Boolean): Builder {
+            this.isOrderedSelection = isOrderedSelection
+            return this
+        }
+
+        /**
+         * Set the default tab for the [PickVisualMediaRequest].
+         *
+         * The default tab is used to open the preferred view inside the photo picker at first such
+         * as, e.g. [DefaultTab.PhotosTab], [DefaultTab.AlbumsTab]. This parameter might be not
+         * supported by the underlying photo picker implementation.
+         *
+         * @param defaultTab the tab to launch the picker in
+         * @return This builder.
+         */
+        fun setDefaultTab(defaultTab: DefaultTab): Builder {
+            this.defaultTab = defaultTab
+            return this
+        }
+
+        /**
+         * Set the accent color for the [PickVisualMediaRequest].
+         *
+         * The accent color is used to change the main color in the photo picker. This parameter
+         * might be not supported by the underlying photo picker implementation.
+         *
+         * @param accentColor color long to apply as accent to the main color in the picker
+         * @return This builder.
+         */
+        fun setAccentColor(accentColor: Long): Builder {
+            this.accentColor = accentColor
+            this.isCustomAccentColorApplied = true
+            return this
+        }
+
+        /**
          * Build the PickVisualMediaRequest specified by this builder.
          *
          * @return the newly constructed PickVisualMediaRequest.
@@ -105,6 +225,10 @@
             PickVisualMediaRequest().apply {
                 this.mediaType = [email protected]
                 this.maxItems = [email protected]
+                this.isOrderedSelection = [email protected]
+                this.defaultTab = [email protected]
+                this.isCustomAccentColorApplied = [email protected]
+                this.accentColor = [email protected]
             }
     }
 }
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 ddc4981..45f8b69 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
@@ -33,6 +33,9 @@
 import androidx.activity.result.PickVisualMediaRequest
 import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.Companion.getClipDataUris
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.ACTION_SYSTEM_FALLBACK_PICK_IMAGES
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.GMS_ACTION_PICK_IMAGES
 import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.GMS_EXTRA_PICK_IMAGES_MAX
@@ -629,6 +632,11 @@
             const val ACTION_SYSTEM_FALLBACK_PICK_IMAGES =
                 "androidx.activity.result.contract.action.PICK_IMAGES"
 
+            internal const val GMS_ACTION_PICK_IMAGES =
+                "com.google.android.gms.provider.action.PICK_IMAGES"
+            internal const val GMS_EXTRA_PICK_IMAGES_MAX =
+                "com.google.android.gms.provider.extra.PICK_IMAGES_MAX"
+
             /**
              * Extra that will be sent by [PickMultipleVisualMedia] to an Activity that handles
              * [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates that maximum number of photos the
@@ -643,10 +651,39 @@
             const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX =
                 "androidx.activity.result.contract.extra.PICK_IMAGES_MAX"
 
-            internal const val GMS_ACTION_PICK_IMAGES =
-                "com.google.android.gms.provider.action.PICK_IMAGES"
-            internal const val GMS_EXTRA_PICK_IMAGES_MAX =
-                "com.google.android.gms.provider.extra.PICK_IMAGES_MAX"
+            /**
+             * Extra that will be sent by [PickVisualMedia] and [PickMultipleVisualMedia] to an
+             * Activity that handles [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates the
+             * preferred default tab of the picker.
+             *
+             * If this extra is not present, the default tab of the picker will be used.
+             */
+            @Suppress("ActionValue")
+            /* Don't include SYSTEM_FALLBACK in the extra */
+            const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB =
+                "androidx.activity.result.contract.extra.PICK_IMAGES_LAUNCH_TAB"
+
+            /**
+             * Extra that will be sent by [PickMultipleVisualMedia] to an Activity that handles
+             * [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates allowing the user to control the
+             * order in which images are returned to the calling app.
+             */
+            @Suppress("ActionValue")
+            /* Don't include SYSTEM_FALLBACK in the extra */
+            const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER =
+                "androidx.activity.result.contract.extra.PICK_IMAGES_IN_ORDER"
+
+            /**
+             * Extra that will be sent by [PickVisualMedia] and [PickMultipleVisualMedia] to an
+             * Activity that handles [ACTION_SYSTEM_FALLBACK_PICK_IMAGES] that indicates the
+             * preferred accent color of the picker.
+             *
+             * If this extra is not present, the default accent color of the picker will be used.
+             */
+            @Suppress("ActionValue")
+            /* Don't include SYSTEM_FALLBACK in the extra */
+            const val EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR =
+                "androidx.activity.result.contract.extra.PICK_IMAGES_ACCENT_COLOR"
 
             /**
              * Check if the current device has support for the photo picker by checking the running
@@ -738,18 +775,46 @@
          */
         class SingleMimeType(val mimeType: String) : VisualMediaType
 
+        /** Represents filter input type accepted by the photo picker. */
+        abstract class DefaultTab private constructor() {
+            abstract val value: Int
+
+            /**
+             * [DefaultTab] object used to open the picker in Photos tab (also the default if no
+             * value is provided).
+             */
+            object PhotosTab : DefaultTab() {
+                override val value = MediaStore.PICK_IMAGES_TAB_IMAGES
+            }
+
+            /** [DefaultTab] object used to open the picker in Albums tab. */
+            object AlbumsTab : DefaultTab() {
+                override val value = MediaStore.PICK_IMAGES_TAB_ALBUMS
+            }
+        }
+
         @CallSuper
         override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
             // Check if Photo Picker is available on the device
             return if (isSystemPickerAvailable()) {
                 Intent(MediaStore.ACTION_PICK_IMAGES).apply {
                     type = getVisualMimeType(input.mediaType)
+                    putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+
+                    if (input.isCustomAccentColorApplied) {
+                        putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+                    }
                 }
             } else if (isSystemFallbackPickerAvailable(context)) {
                 val fallbackPicker = checkNotNull(getSystemFallbackPicker(context)).activityInfo
                 Intent(ACTION_SYSTEM_FALLBACK_PICK_IMAGES).apply {
                     setClassName(fallbackPicker.applicationInfo.packageName, fallbackPicker.name)
                     type = getVisualMimeType(input.mediaType)
+                    putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+
+                    if (input.isCustomAccentColorApplied) {
+                        putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+                    }
                 }
             } else if (isGmsPickerAvailable(context)) {
                 val gmsPicker = checkNotNull(getGmsPicker(context)).activityInfo
@@ -847,6 +912,12 @@
                     }
 
                     putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, currentMaxItems)
+                    putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+                    putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, input.isOrderedSelection)
+
+                    if (input.isCustomAccentColorApplied) {
+                        putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+                    }
                 }
             } else if (PickVisualMedia.isSystemFallbackPickerAvailable(context)) {
                 val fallbackPicker = checkNotNull(getSystemFallbackPicker(context)).activityInfo
@@ -858,6 +929,12 @@
                     require(currentMaxItems > 1) { "Max items must be greater than 1" }
 
                     putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_MAX, currentMaxItems)
+                    putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_LAUNCH_TAB, input.defaultTab.value)
+                    putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_IN_ORDER, input.isOrderedSelection)
+
+                    if (input.isCustomAccentColorApplied) {
+                        putExtra(EXTRA_SYSTEM_FALLBACK_PICK_IMAGES_ACCENT_COLOR, input.accentColor)
+                    }
                 }
             } else if (PickVisualMedia.isGmsPickerAvailable(context)) {
                 val gmsPicker = checkNotNull(getGmsPicker(context)).activityInfo
diff --git a/activity/integration-tests/macrobenchmark-target/build.gradle b/activity/integration-tests/macrobenchmark-target/build.gradle
index 68c0b8a..e984599 100644
--- a/activity/integration-tests/macrobenchmark-target/build.gradle
+++ b/activity/integration-tests/macrobenchmark-target/build.gradle
@@ -12,7 +12,8 @@
 }
 
 android {
-    namespace "androidx.activity.integration.macrobenchmark.target"
+   compileSdk 35
+   namespace "androidx.activity.integration.macrobenchmark.target"
 }
 
 dependencies {
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index 09b6006..7fbf614 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         applicationId "androidx.activity.integration.testapp"
     }
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 9ab8601..5b0c417 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
@@ -131,19 +131,40 @@
                         PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif"))
                     )
                 }
+                button("Pick an image & show albums tab (w/ photo picker)") {
+                    pickVisualMedia.launch(
+                        PickVisualMediaRequest(
+                            mediaType = PickVisualMedia.ImageOnly,
+                            defaultTab = PickVisualMedia.DefaultTab.AlbumsTab
+                        )
+                    )
+                }
+                button("Pick an image & green accent color (w/ photo picker)") {
+                    pickVisualMedia.launch(
+                        PickVisualMediaRequest(
+                            mediaType = PickVisualMedia.ImageOnly,
+                            accentColor = 0xFF123456
+                        )
+                    )
+                }
                 button("Pick 5 visual media max (w/ photo picker)") {
                     pickMultipleVisualMedia.launch(
                         PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
                     )
                 }
-                button("Pick 9 visual media max (w/ photo picker)") {
+                button("Pick 3 visual media max (w/ photo picker)") {
                     pickMultipleVisualMedia.launch(
                         PickVisualMediaRequest(
                             mediaType = PickVisualMedia.ImageAndVideo,
-                            maxItems = 9
+                            maxItems = 3
                         )
                     )
                 }
+                button("Pick 5 visual media max (w/ photo picker) & selection order") {
+                    pickMultipleVisualMedia.launch(
+                        PickVisualMediaRequest(isOrderedSelection = true)
+                    )
+                }
                 button("Create document") { createDocument.launch("Temp") }
                 button("Open documents") { openDocuments.launch(arrayOf("*/*")) }
                 button("Start IntentSender") {
diff --git a/appcompat/appcompat-lint/integration-tests/build.gradle b/appcompat/appcompat-lint/integration-tests/build.gradle
index cd7691f..4c2adc6 100644
--- a/appcompat/appcompat-lint/integration-tests/build.gradle
+++ b/appcompat/appcompat-lint/integration-tests/build.gradle
@@ -18,6 +18,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         vectorDrawables.useSupportLibrary = true
     }
diff --git a/appcompat/integration-tests/receive-content-testapp/build.gradle b/appcompat/integration-tests/receive-content-testapp/build.gradle
index e414862..4c0dcc7 100644
--- a/appcompat/integration-tests/receive-content-testapp/build.gradle
+++ b/appcompat/integration-tests/receive-content-testapp/build.gradle
@@ -20,6 +20,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.appcompat.demo.receivecontent"
 }
 
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
index 9d17a58..d98a917 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
@@ -23,11 +23,13 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.util.Preconditions;
 
@@ -61,6 +63,9 @@
             + "Please use androidx.appsearch.app.ShortcutAdapter.DEFAULT_NAMESPACE as the "
             + "namespace of the document if it will be used to create a shortcut.";
 
+    private static final String APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE =
+            "appsearch_generic_doc_parcel";
+
     /**
      * Converts given document to a {@link ShortcutInfoCompat.Builder}, which can be used to
      * construct a shortcut for donation through
@@ -117,16 +122,19 @@
             throw new IllegalArgumentException(NAMESPACE_CHECK_ERROR_MESSAGE);
         }
         final String name = doc.getPropertyString(FIELD_NAME);
+        final Bundle extras = new Bundle();
+        extras.putParcelable(APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, doc.getDocumentParcel());
         return new ShortcutInfoCompat.Builder(context, doc.getId())
                 .setShortLabel(!TextUtils.isEmpty(name) ? name : doc.getId())
                 .setIntent(new Intent(Intent.ACTION_VIEW, getDocumentUri(doc)))
                 .setExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)
-                .setTransientExtras(doc.getBundle());
+                .setTransientExtras(extras);
     }
 
     /**
      * Extracts {@link GenericDocument} from given {@link ShortcutInfoCompat} if applicable.
      * Returns null if document cannot be found in the given shortcut.
+     *
      * @exportToFramework:hide
      */
     @Nullable
@@ -137,7 +145,21 @@
         if (extras == null) {
             return null;
         }
-        return new GenericDocument(extras);
+
+        GenericDocumentParcel genericDocParcel;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            genericDocParcel = Api33Impl.getParcelableFromBundle(extras,
+                    APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, GenericDocumentParcel.class);
+        } else {
+            @SuppressWarnings("deprecation")
+            GenericDocumentParcel tmp = (GenericDocumentParcel) extras.getParcelable(
+                    APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE);
+            genericDocParcel = tmp;
+        }
+        if (genericDocParcel == null) {
+            return null;
+        }
+        return new GenericDocument(genericDocParcel);
     }
 
     /**
@@ -177,4 +199,21 @@
                 .path(DEFAULT_NAMESPACE + "/" + id)
                 .build();
     }
+    @RequiresApi(33)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static <T> T getParcelableFromBundle(
+                @NonNull Bundle bundle,
+                @NonNull String key,
+                @NonNull Class<T> clazz) {
+            Preconditions.checkNotNull(bundle);
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(clazz);
+            return bundle.getParcelable(key, clazz);
+        }
+    }
 }
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
index 2c40844..f84153b 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
@@ -73,6 +73,6 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mAsText);
+        return Objects.hashCode(mAsText);
     }
 }
diff --git a/appsearch/appsearch-debug-view/build.gradle b/appsearch/appsearch-debug-view/build.gradle
index 5d774b7..5472f31 100644
--- a/appsearch/appsearch-debug-view/build.gradle
+++ b/appsearch/appsearch-debug-view/build.gradle
@@ -29,6 +29,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.appsearch.debugview"
 }
 
diff --git a/appsearch/appsearch-debug-view/samples/build.gradle b/appsearch/appsearch-debug-view/samples/build.gradle
index 25b9618..3321369 100644
--- a/appsearch/appsearch-debug-view/samples/build.gradle
+++ b/appsearch/appsearch-debug-view/samples/build.gradle
@@ -29,6 +29,7 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "androidx.appsearch.debugview.samples"
 }
 
diff --git a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
index 5d84149..17f6a11 100644
--- a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
+++ b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
@@ -31,6 +31,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.debugview.samples.model.Note;
 import androidx.appsearch.debugview.view.AppSearchDebugActivity;
 import androidx.core.content.ContextCompat;
@@ -49,7 +50,6 @@
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executors;
 
 /**
  * Default Activity for AppSearch Debug View Sample App
@@ -80,7 +80,8 @@
         mListView = findViewById(R.id.list_view);
         mLoadingView = findViewById(R.id.text_view);
 
-        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+                .getEnvironmentInstance().createCachedThreadPoolExecutor());
 
         mNotesAppSearchManagerFuture.setFuture(NotesAppSearchManager.createNotesAppSearchManager(
                 getApplicationContext(), mBackgroundExecutor));
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 a6a79da..79a4c6b 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
@@ -165,7 +165,7 @@
         SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
                 .setResultCountPerPage(PAGE_SIZE)
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList());
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList());
         String retrieveAllQueryString = "";
 
         if (mSearchType == AppSearchDebugActivity.SEARCH_TYPE_GLOBAL) {
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 8c979f8..08320075 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
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.debugview.DebugAppSearchManager;
 import androidx.appsearch.debugview.R;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -36,7 +37,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.Executors;
 
 /**
  * Debug Activity for AppSearch.
@@ -105,7 +105,8 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_appsearchdebug);
 
-        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+                .getEnvironmentInstance().createCachedThreadPoolExecutor());
         mDbName = getIntent().getExtras().getString(DB_INTENT_KEY);
         String targetPackageName =
                 getIntent().getExtras().getString(TARGET_PACKAGE_NAME_INTENT_KEY);
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 3833958..ae89b19 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
@@ -20,6 +20,10 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_PACKAGE_NAME;
+import static androidx.appsearch.testutil.AppSearchTestUtils.createMockVisibilityChecker;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -27,11 +31,13 @@
 
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResult;
@@ -41,7 +47,6 @@
 import androidx.appsearch.app.SearchSuggestionSpec;
 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;
@@ -49,6 +54,7 @@
 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityToDocumentConverter;
 import androidx.appsearch.observer.DocumentChangeInfo;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.observer.SchemaChangeInfo;
@@ -58,6 +64,9 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.FlakyTest;
 
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
 import com.google.android.icing.proto.DebugInfoProto;
 import com.google.android.icing.proto.DebugInfoVerbosity;
 import com.google.android.icing.proto.DocumentProto;
@@ -72,7 +81,9 @@
 import com.google.android.icing.proto.StorageInfoProto;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
+import com.google.android.icing.protobuf.ByteString;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -117,8 +128,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
@@ -400,7 +411,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -454,7 +465,7 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -502,7 +513,7 @@
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
 
         // Check recovery state
         InitializeStats initStats = initStatsBuilder.build();
@@ -539,7 +550,7 @@
                 mContext.getPackageName(),
                 "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -585,7 +596,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -598,7 +609,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -650,7 +661,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -663,7 +674,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -727,8 +738,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         // We need to share across packages
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -736,8 +746,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Insert package1 schema
         List<AppSearchSchema> personSchema =
@@ -746,7 +756,7 @@
                 "package1",
                 "database1",
                 personSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -764,7 +774,7 @@
                 "package2",
                 "database2",
                 callSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -777,7 +787,7 @@
                 "package3",
                 "database3",
                 textSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -903,8 +913,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         // We need to share across packages
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -912,8 +921,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         AppSearchSchema.StringPropertyConfig personField =
                 new AppSearchSchema.StringPropertyConfig.Builder("personId")
@@ -931,7 +940,7 @@
                 "package1",
                 "database1",
                 personAndCallSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -945,7 +954,7 @@
                 "package2",
                 "database2",
                 callSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1107,7 +1116,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1166,7 +1175,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1215,7 +1224,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1273,7 +1282,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1334,7 +1343,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1392,7 +1401,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1423,7 +1432,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1476,7 +1485,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1539,7 +1548,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1595,7 +1604,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1661,7 +1670,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1721,7 +1730,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1765,7 +1774,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1828,7 +1837,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1891,7 +1900,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1979,7 +1988,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1989,7 +1998,9 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
@@ -2016,7 +2027,7 @@
                 "package",
                 "database1",
                 oldSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2031,7 +2042,7 @@
                 "package",
                 "database1",
                 newSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2055,7 +2066,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2065,9 +2076,14 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Document")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         // Check both schema Email and Document saved correctly.
@@ -2083,7 +2099,7 @@
                         "package",
                         "database1",
                         finalSchemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
+                        /*visibilityConfigs=*/ Collections.emptyList(),
                         /*forceOverride=*/ false,
                         /*version=*/ 0,
                         /* setSchemaStatsBuilder= */ null);
@@ -2098,7 +2114,7 @@
                 "package",
                 "database1",
                 finalSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2108,7 +2124,9 @@
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         expectedTypes = new ArrayList<>();
@@ -2133,7 +2151,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2142,7 +2160,7 @@
                 "package",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2152,14 +2170,24 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database2/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document").setVersion(0))
+                                .setSchemaType("package$database1/Document")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Email")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Document")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         // Check Email and Document is saved in database 1 and 2 correctly.
@@ -2175,7 +2203,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2186,12 +2214,19 @@
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database2/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document").setVersion(0))
+                                .setSchemaType("package$database2/Email")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Document")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         // Check nothing changed in database2.
@@ -2215,7 +2250,7 @@
                 "package",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2265,8 +2300,8 @@
             existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
         }
 
-        // Create VisibilityDocument
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("schema")
+        // Create VisibilityConfig
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -2278,7 +2313,7 @@
                 "packageA",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2287,7 +2322,7 @@
                 "packageB",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2297,10 +2332,14 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("packageA$database/schema").setVersion(0))
+                                .setSchemaType("packageA$database/schema")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("packageB$database/schema").setVersion(0))
+                                .setSchemaType("packageB$database/schema")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
         expectedTypes.addAll(existingSchemas);
@@ -2309,22 +2348,22 @@
                 .containsExactlyElementsIn(expectedTypes);
 
         // Verify these two visibility documents are stored in AppSearch.
-        VisibilityDocument expectedVisibilityDocumentA =
-                new VisibilityDocument.Builder("packageA$database/schema")
+        InternalVisibilityConfig expectedVisibilityConfigA =
+                new InternalVisibilityConfig.Builder("packageA$database/schema")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                         .build();
-        VisibilityDocument expectedVisibilityDocumentB =
-                new VisibilityDocument.Builder("packageB$database/schema")
+        InternalVisibilityConfig expectedVisibilityConfigB =
+                new InternalVisibilityConfig.Builder("packageB$database/schema")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                         .build();
         assertThat(mAppSearchImpl.mVisibilityStoreLocked
                 .getVisibility("packageA$database/schema"))
-                .isEqualTo(expectedVisibilityDocumentA);
+                .isEqualTo(expectedVisibilityConfigA);
         assertThat(mAppSearchImpl.mVisibilityStoreLocked
                 .getVisibility("packageB$database/schema"))
-                .isEqualTo(expectedVisibilityDocumentB);
+                .isEqualTo(expectedVisibilityConfigB);
 
         // Prune packages
         mAppSearchImpl.prunePackageData(existingPackages);
@@ -2353,7 +2392,7 @@
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package1", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2366,7 +2405,7 @@
         internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package1", "database2",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2379,7 +2418,7 @@
         internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package2", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2401,7 +2440,7 @@
                 "package1",
                 "database1",
                 schemas1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2410,7 +2449,7 @@
                 "package1",
                 "database2",
                 schemas2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2419,7 +2458,7 @@
                 "package2",
                 "database1",
                 schemas3,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2429,7 +2468,8 @@
                 "package1$database2/type2",
                 "package2$database1/type3",
                 "VS#Pkg$VS#Db/VisibilityType",  // plus the stored Visibility schema
-                "VS#Pkg$VS#Db/VisibilityPermissionType");
+                "VS#Pkg$VS#Db/VisibilityPermissionType",
+                "VS#Pkg$VS#AndroidVDb/AndroidVOverlayType");
     }
 
     @FlakyTest(bugId = 204186664)
@@ -2442,7 +2482,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2541,7 +2581,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2564,7 +2604,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2585,7 +2625,7 @@
                 "package2",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2643,7 +2683,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2666,7 +2706,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2688,7 +2728,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2697,7 +2737,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2755,7 +2795,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2768,7 +2808,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null));
@@ -2840,7 +2880,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2870,8 +2910,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
                 "id1",
                 Collections.emptyMap());
@@ -2887,7 +2927,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2942,8 +2982,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -2964,7 +3004,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3021,8 +3061,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -3043,7 +3083,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3077,7 +3117,7 @@
                 .isEqualTo(2);
         assertThat(
                 storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
-                .isEqualTo(3); // +2 for VisibilitySchema
+                .isEqualTo(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
     }
 
     @Test
@@ -3088,7 +3128,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3122,7 +3162,7 @@
                 debugInfo.getDocumentInfo().getDocumentStorageInfo().getNumAliveDocuments())
                 .isEqualTo(2);
         assertThat(debugInfo.getSchemaInfo().getSchema().getTypesList())
-                .hasSize(3); // +2 for VisibilitySchema
+                .hasSize(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
     }
 
     @Test
@@ -3146,8 +3186,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -3156,7 +3196,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3225,8 +3265,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -3235,7 +3275,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3282,8 +3322,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Make sure the limit is maintained
         e = assertThrows(AppSearchException.class, () ->
@@ -3319,8 +3359,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -3329,7 +3369,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3432,8 +3472,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas =
@@ -3442,7 +3482,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3451,7 +3491,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3460,7 +3500,7 @@
                 "package2",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3469,7 +3509,7 @@
                 "package2",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3528,8 +3568,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // package1 should still be out of space
         e = assertThrows(AppSearchException.class, () ->
@@ -3585,8 +3625,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3602,7 +3642,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3738,8 +3778,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3751,7 +3791,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3821,8 +3861,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List<AppSearchSchema> schemas = Collections.singletonList(
@@ -3834,7 +3874,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3878,8 +3918,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Index id2. This should pass but only because we check for replacements.
         mAppSearchImpl.putDocument(
@@ -3924,8 +3964,8 @@
                         return 2;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         AppSearchException e = assertThrows(AppSearchException.class, () ->
                 mAppSearchImpl.searchSuggestion(
@@ -3950,7 +3990,7 @@
                 mContext.getPackageName(),
                 "database1",
                 /*schemas=*/ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/false,
                 /*version=*/0,
                 /*setSchemaStatsBuilder=*/null);
@@ -4020,8 +4060,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(false);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4029,14 +4068,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4073,8 +4112,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4082,14 +4120,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4124,8 +4162,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4133,14 +4170,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4176,9 +4213,20 @@
         // 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");
+        VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals("visiblePackage");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4186,14 +4234,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4237,7 +4285,7 @@
 
     @Test
     public void testSetVisibility() throws Exception {
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4249,7 +4297,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4257,27 +4305,31 @@
         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]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
     }
 
     @Test
     public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
         // Create Visibility Document for Email1
-        VisibilityDocument visibilityDocument1 = new VisibilityDocument.Builder("Email1")
+        InternalVisibilityConfig visibilityConfig1 = new InternalVisibilityConfig.Builder("Email1")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4289,7 +4341,7 @@
                 "package1",
                 "database",
                 schemas1,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument1),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig1),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4297,24 +4349,29 @@
         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]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+        InternalVisibilityConfig expectedDocument1 =
+                new InternalVisibilityConfig.Builder(prefix1 + "Email1")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix1 + "Email1"))
                 .isEqualTo(expectedDocument1);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument1 =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix1 + "Email1",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument1 =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix1 + "Email1",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
+
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
 
         // Create Visibility Document for Email2
-        VisibilityDocument visibilityDocument2 = new VisibilityDocument.Builder("Email2")
+        InternalVisibilityConfig visibilityConfig2 = new InternalVisibilityConfig.Builder("Email2")
                 .setNotDisplayedBySystem(false)
                 .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
                 .build();
@@ -4326,7 +4383,7 @@
                 "package2",
                 "database",
                 schemas2,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument2),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig2),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4334,39 +4391,46 @@
         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]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
+        InternalVisibilityConfig expectedDocument2 =
+                new InternalVisibilityConfig.Builder(prefix2 + "Email2")
+                        .setNotDisplayedBySystem(false)
+                        .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix2 + "Email2"))
                 .isEqualTo(expectedDocument2);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument2 =  new VisibilityDocument.Builder(
-                mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix2 + "Email2",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument2 =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix2 + "Email2",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument2).isEqualTo(expectedDocument2);
 
         // Check the existing visibility document retains.
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix1 + "Email1"))
                 .isEqualTo(expectedDocument1);
         // Verify the VisibilityDocument is saved to AppSearchImpl.
-        actualDocument1 =  new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix1 + "Email1",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        actualDocument1 = VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                mAppSearchImpl.getDocument(
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                        /*id=*/ prefix1 + "Email1",
+                        /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
     }
 
     @Test
     public void testSetVisibility_removeVisibilitySettings() throws Exception {
         // Create a non-all-default visibility document
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4379,26 +4443,29 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix = PrefixUtil.createPrefix("package", "database1");
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // Set schema Email and its all-default visibility document to AppSearch database1
@@ -4406,7 +4473,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4414,12 +4481,12 @@
         // All-default visibility document won't be saved in AppSearch.
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
                 .isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4429,7 +4496,7 @@
     @Test
     public void testRemoveVisibility_noRemainingSettings() throws Exception {
         // Create a non-all-default visibility document
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4442,25 +4509,29 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*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]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove the schema and visibility setting from AppSearch
@@ -4468,7 +4539,7 @@
                 "package",
                 "database1",
                 /*schemas=*/ new ArrayList<>(),
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4478,7 +4549,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4488,9 +4559,9 @@
         // Verify there is no visibility setting for the schema.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4500,7 +4571,7 @@
     @Test
     public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
         // set Schema and visibility to AppSearch
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4510,7 +4581,7 @@
                 "packageName",
                 "databaseName",
                 schemas,
-                ImmutableList.of(visibilityDocument),
+                ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4525,25 +4596,29 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
 
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove schema and visibility document
@@ -4566,16 +4641,16 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4590,8 +4665,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4599,16 +4673,16 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type that is not displayed by the system
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("Type")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type")
                                 .setNotDisplayedBySystem(true).build()),
                 /*forceOverride=*/false,
                 /*version=*/0,
@@ -4631,7 +4705,7 @@
                 "package",
                 "database",
                 Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
-                /*visibilityDocuments=*/ImmutableList.of(),
+                /*visibilityConfigs=*/ImmutableList.of(),
                 /*forceOverride=*/false,
                 /*version=*/0,
                 /*setSchemaStatsBuilder=*/null);
@@ -4655,7 +4729,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(),
+                /*visibilityConfigs=*/ImmutableList.of(),
                 /*forceOverride=*/false,
                 /*version=*/1,
                 /*setSchemaStatsBuilder=*/null);
@@ -4687,9 +4761,20 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> prefixedSchema.endsWith("VisibleType");
+        VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return prefixedSchema.endsWith("VisibleType");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4697,19 +4782,19 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add two schema types that are not displayed by the system.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("VisibleType")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("VisibleType")
                                 .setNotDisplayedBySystem(true)
                                 .build(),
-                        new VisibilityDocument.Builder("PrivateType")
+                        new InternalVisibilityConfig.Builder("PrivateType")
                                 .setNotDisplayedBySystem(true)
                                 .build()),
                 /*forceOverride=*/false,
@@ -4728,13 +4813,258 @@
     }
 
     @Test
+    public void testGetSchema_global_publicAcl() throws Exception {
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("PublicTypeA").build(),
+                new AppSearchSchema.Builder("PublicTypeB").build(),
+                new AppSearchSchema.Builder("PublicTypeC").build());
+
+        PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+        PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+        PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+
+        // Package A is visible to package B & C, package B is visible to package C (based on
+        // canPackageQuery, which we are mocking).
+        Map<String, Set<String>> packageCanSee = ImmutableMap.of(
+                "A", ImmutableSet.of("A"),
+                "B", ImmutableSet.of("A", "B"),
+                "C", ImmutableSet.of("A", "B", "C"));
+        final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+                return packageCanSee.get(callerAccess.getCallingPackageName())
+                        .contains(param.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                                .getPackageName());
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/ null,
+                publicAclMockChecker, ALWAYS_OPTIMIZE
+        );
+
+        List<InternalVisibilityConfig> visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA")
+                        .setPubliclyVisibleTargetPackage(pkgA).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB")
+                        .setPubliclyVisibleTargetPackage(pkgB).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC")
+                        .setPubliclyVisibleTargetPackage(pkgC).build());
+
+        // Add the three schema types, each with their own publicly visible target package.
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Verify access to schemas based on calling package
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgA.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+
+        getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgB.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0), schemas.get(1));
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+
+        getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgC.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeC");
+    }
+
+    @Test
+    public void testGetSchema_global_publicAcl_removal() throws Exception {
+        // This test to ensure the proper documents are created through setSchema, then removed
+        // when setSchema is called again
+        List<AppSearchSchema> schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("PublicTypeA").build(),
+                new AppSearchSchema.Builder("PublicTypeB").build(),
+                new AppSearchSchema.Builder("PublicTypeC").build());
+
+        PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+        PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+        PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+
+        // Package A is visible to package B & C, package B is visible to package C (based on
+        // canPackageQuery, which we are mocking).
+        Map<String, Set<String>> packageCanSee = ImmutableMap.of(
+                "A", ImmutableSet.of("A"),
+                "B", ImmutableSet.of("A", "B"),
+                "C", ImmutableSet.of("A", "B", "C"));
+        final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+                return packageCanSee.get(callerAccess.getCallingPackageName())
+                        .contains(param.getVisibilityConfig()
+                                .getPubliclyVisibleTargetPackage().getPackageName());
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/ null,
+                publicAclMockChecker, ALWAYS_OPTIMIZE
+        );
+
+        List<InternalVisibilityConfig> visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA")
+                        .setPubliclyVisibleTargetPackage(pkgA).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB")
+                        .setPubliclyVisibleTargetPackage(pkgB).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC")
+                        .setPubliclyVisibleTargetPackage(pkgC).build());
+
+        // Add two schema types that are not displayed by the system.
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Now check for documents
+        GenericDocument visibilityOverlayA = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeA",
+                Collections.emptyMap());
+        GenericDocument visibilityOverlayB = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeB",
+                Collections.emptyMap());
+        GenericDocument visibilityOverlayC = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeC",
+                Collections.emptyMap());
+
+        AndroidVOverlayProto overlayProtoA = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("A")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+        AndroidVOverlayProto overlayProtoB = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("B")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+        AndroidVOverlayProto overlayProtoC = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("C")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+
+        assertThat(visibilityOverlayA.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoA.toByteArray());
+        assertThat(visibilityOverlayB.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoB.toByteArray());
+        assertThat(visibilityOverlayC.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoC.toByteArray());
+
+        // now undo the "public" setting
+        visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA").build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB").build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC").build());
+
+        InternalSetSchemaResponse internalSetSchemaResponseRemoved = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponseRemoved.isSuccess()).isTrue();
+
+        // Now check for documents again
+        Exception e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeA", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeB", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeC", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+    }
+
+    @Test
     public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4776,8 +5106,7 @@
     @Test
     public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
         // Make a visibility checker that rejects everything
-        final VisibilityChecker rejectChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4786,15 +5115,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                rejectChecker);
+                rejectChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4840,7 +5169,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4850,7 +5179,7 @@
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
                 new CallerAccess(/*callingPackageName=*/
-                    "com.fake.Listening.package"),
+                        "com.fake.Listening.package"),
                 /*targetPackageName=*/mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
@@ -4879,9 +5208,19 @@
         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);
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4890,15 +5229,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4942,8 +5281,7 @@
         final String fakeListeningPackage = "com.fake.Listening.package";
 
         // Make a visibility checker that rejects everything.
-        final VisibilityChecker rejectChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4952,15 +5290,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                rejectChecker);
+                rejectChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5011,7 +5349,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5037,7 +5375,7 @@
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build(),
                         new AppSearchSchema.Builder("Type3").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5062,7 +5400,7 @@
                 ImmutableList.of(
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5082,7 +5420,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5115,7 +5453,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5143,7 +5481,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 1,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5170,7 +5508,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 2,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5207,7 +5545,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5241,7 +5579,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5262,16 +5600,30 @@
         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;
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
+                    return false;
+                }
+
+                for (PackageIdentifier packageIdentifier :
+                        visibilityStore.getVisibility(prefixedSchema)
+                                .getVisibilityConfig().getAllowedPackages()) {
+                    if (packageIdentifier.getPackageName().equals(fakeListeningPackage)) {
+                        return true;
                     }
-                    Set<String> allowedPackages = new ArraySet<>(
-                            visibilityStore.getVisibility(prefixedSchema).getPackageNames());
-                    return allowedPackages.contains(fakeListeningPackage);
-                };
+                }
+                return false;
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5280,8 +5632,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
@@ -5300,16 +5652,15 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2")
+                        new InternalVisibilityConfig.Builder("Type2")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
-                                .build()
-                ),
+                                .build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5330,12 +5681,12 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2").build()
+                        new InternalVisibilityConfig.Builder("Type2").build()
                 ),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
@@ -5365,13 +5716,12 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2").build()
-                ),
+                        new InternalVisibilityConfig.Builder("Type2").build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5396,16 +5746,15 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2")
+                        new InternalVisibilityConfig.Builder("Type2")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
-                                .build()
-                ),
+                                .build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5426,10 +5775,20 @@
         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)
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
                         && prefixedSchema.endsWith("Type2");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5438,8 +5797,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5460,7 +5819,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5494,7 +5853,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5515,10 +5874,20 @@
         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)
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
                         && prefixedSchema.endsWith("Type2");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5527,8 +5896,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5537,7 +5906,7 @@
                 ImmutableList.of(
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5557,7 +5926,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5575,7 +5944,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5597,21 +5966,29 @@
 
         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);
-                    }
-                };
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore 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);
+                }
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5620,8 +5997,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5633,7 +6010,7 @@
                         new AppSearchSchema.Builder("Type3").build(),
                         new AppSearchSchema.Builder("Type4").build()
                 ),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5669,7 +6046,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5724,7 +6101,7 @@
                                                 .build()
                                 ).build()
                 ),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 1,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5761,7 +6138,7 @@
                 mContext.getPackageName(),
                 "database1",
                 updatedSchemaTypes,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 2,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5783,7 +6160,7 @@
                 mContext.getPackageName(),
                 "database1",
                 updatedSchemaTypes,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 3,
                 /*setSchemaStatsBuilder=*/ null);
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 e9b8e47..aa4c96a 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
@@ -80,8 +80,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         mLogger = new SimpleTestLogger();
     }
 
@@ -373,8 +373,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 initStatsBuilder,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
         appSearchImpl.close();
 
@@ -406,8 +406,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
                 new AppSearchSchema.Builder("Type2").build());
@@ -443,7 +443,7 @@
         appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
         assertThat(iStats).isNotNull();
@@ -456,7 +456,8 @@
         assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
                 InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
         assertThat(iStats.getDocumentCount()).isEqualTo(2);
-        assertThat(iStats.getSchemaTypeCount()).isEqualTo(4); // +2 for VisibilitySchema
+        // Type1 + Type2 +2 for VisibilitySchema, +1 for VisibilityOverlay
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(5);
         assertThat(iStats.hasReset()).isEqualTo(false);
         assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
         appSearchImpl.close();
@@ -471,7 +472,7 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
 
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
@@ -511,7 +512,7 @@
         appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
         // Some of other fields are already covered by AppSearchImplTest#testReset()
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java
new file mode 100644
index 0000000..fadf4bf
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SchemaCacheTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage;
+
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public class SchemaCacheTest {
+
+    @Test
+    public void testGetSchemaTypesWithDescendants() throws Exception {
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Other")
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/Person", personSchema,
+                        "package$database/Artist", artistSchema,
+                        "package$database/Other", otherSchema));
+        SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/Person"))).containsExactly(
+                "package$database/Person", "package$database/Artist");
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/Artist"))).containsExactly(
+                "package$database/Artist");
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/Other"))).containsExactly(
+                "package$database/Other");
+    }
+
+    @Test
+    public void testGetSchemaTypesWithDescendants_multipleLevel() throws Exception {
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto schemaA =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaB =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/B")
+                        .build();
+        SchemaTypeConfigProto schemaC =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/C")
+                        .addParentTypes("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaD =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/D")
+                        .addParentTypes("package$database/C")
+                        .build();
+        SchemaTypeConfigProto schemaE =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/E")
+                        .addParentTypes("package$database/B")
+                        .addParentTypes("package$database/C")
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/A", schemaA,
+                        "package$database/B", schemaB,
+                        "package$database/C", schemaC,
+                        "package$database/D", schemaD,
+                        "package$database/E", schemaE));
+        SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/A"))).containsExactly(
+                "package$database/A", "package$database/C", "package$database/D",
+                "package$database/E");
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/B"))).containsExactly(
+                "package$database/B", "package$database/E");
+        assertThat(schemaCache.getSchemaTypesWithDescendants(prefix,
+                ImmutableSet.of("package$database/A", "package$database/B"))).containsExactly(
+                "package$database/A", "package$database/B", "package$database/C",
+                "package$database/D", "package$database/E");
+    }
+}
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 0690cd5..f189baf 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
@@ -55,8 +55,8 @@
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index a8bb683..ad33c41 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -33,6 +34,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -44,6 +46,10 @@
     private static final byte[] BYTE_ARRAY_2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
     private static final String SCHEMA_TYPE_1 = "sDocumentPropertiesSchemaType1";
     private static final String SCHEMA_TYPE_2 = "sDocumentPropertiesSchemaType2";
+    private static final EmbeddingVector sEmbedding1 = new EmbeddingVector(
+            new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+    private static final EmbeddingVector sEmbedding2 = new EmbeddingVector(
+            new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
     private static final GenericDocument DOCUMENT_PROPERTIES_1 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
                     "namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
@@ -429,4 +435,59 @@
                 expectedDocWithParentAsMetaField);
     }
     // @exportToFramework:endStrip()
+
+    @Test
+    public void testDocumentProtoConvert_EmbeddingProperty() throws Exception {
+        GenericDocument document =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES_1)
+                        .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                        .build();
+
+        // Create the Document proto. Need to sort the property order by key.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setScore(1)
+                .setTtlMs(1L)
+                .setNamespace("namespace");
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put("longKey1",
+                PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put("documentKey1",
+                PropertyProto.newBuilder().setName("documentKey1").addDocumentValues(
+                        GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_1)));
+        propertyProtoMap.put("embeddingKey1",
+                PropertyProto.newBuilder().setName("embeddingKey1")
+                        .addVectorValues(PropertyProto.VectorProto.newBuilder()
+                                .addAllValues(Arrays.asList(1.1f, 2.2f, 3.3f))
+                                .setModelSignature("my_model_v1")
+                        )
+                        .addVectorValues(PropertyProto.VectorProto.newBuilder()
+                                .addAllValues(Arrays.asList(4.4f, 5.5f, 6.6f, 7.7f))
+                                .setModelSignature("my_model_v2")
+                        ));
+        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
+        Collections.sort(sortedKey);
+        for (String key : sortedKey) {
+            documentProtoBuilder.addProperties(propertyProtoMap.get(key));
+        }
+        DocumentProto documentProto = documentProtoBuilder.build();
+
+        GenericDocument convertedGenericDocument =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                                new LocalStorageIcingOptionsConfig()));
+        DocumentProto convertedDocumentProto =
+                GenericDocumentToProtoConverter.toDocumentProto(document);
+
+        assertThat(convertedDocumentProto).isEqualTo(documentProto);
+        assertThat(convertedGenericDocument).isEqualTo(document);
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 4513357..bcef397 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -21,6 +21,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 
 import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
 import com.google.android.icing.proto.JoinableConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -33,6 +34,128 @@
 
 public class SchemaToProtoConverterTest {
     @Test
+    public void testGetProto_DescriptionSet() {
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setDescription("The most important part.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("timestamp")
+                                        .setDescription("The time at which the email was sent.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DoublePropertyConfig.Builder("importanceScore")
+                                        .setDescription(
+                                                "A value representing this document's importance.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BooleanPropertyConfig.Builder("read")
+                                        .setDescription(
+                                                "Whether the email has been read by the recipient")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BytesPropertyConfig.Builder("attachment")
+                                        .setDescription("Documents that are attached to the email.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .build())
+                        // We don't need to actually define the Person type for this test because
+                        // the converter will process each schema individually.
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "sender", "Person")
+                                        .setDescription("The person who wrote this email.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .build();
+
+        SchemaTypeConfigProto expectedEmailProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("Email")
+                        .setDescription("A type of electronic message.")
+                        .setVersion(12345)
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("subject")
+                                        .setDescription("The most important part.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setStringIndexingConfig(
+                                                StringIndexingConfig.newBuilder()
+                                                        .setTokenizerType(
+                                                                StringIndexingConfig.TokenizerType
+                                                                        .Code.PLAIN)
+                                                        .setTermMatchType(
+                                                                TermMatchType.Code.PREFIX)))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("timestamp")
+                                        .setDescription("The time at which the email was sent.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.INT64)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("importanceScore")
+                                        .setDescription(
+                                                "A value representing this document's importance.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.DOUBLE)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("read")
+                                        .setDescription(
+                                                "Whether the email has been read by the recipient")
+                                        .setDataType(PropertyConfigProto.DataType.Code.BOOLEAN)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("attachment")
+                                        .setDescription("Documents that are attached to the email.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.BYTES)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.REPEATED))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("sender")
+                                        .setSchemaType("Person")
+                                        .setDescription("The person who wrote this email.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setDocumentIndexingConfig(
+                                                DocumentIndexingConfig.newBuilder()
+                                                        .setIndexNestedProperties(false)))
+                        .build();
+
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/ 12345))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
+    }
+
+    @Test
     public void testGetProto_Email() {
         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
@@ -53,9 +176,11 @@
 
         SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("Email")
+                .setDescription("")
                 .setVersion(12345)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("subject")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                         .setStringIndexingConfig(
@@ -66,6 +191,7 @@
                         )
                 ).addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("body")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                         .setStringIndexingConfig(
@@ -99,9 +225,11 @@
 
         SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("MusicRecording")
+                .setDescription("")
                 .setVersion(0)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("artist")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
                         .setStringIndexingConfig(
@@ -112,6 +240,7 @@
                         )
                 ).addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("pubDate")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.INT64)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                 ).build();
@@ -130,28 +259,21 @@
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setJoinableValueType(AppSearchSchema.StringPropertyConfig
                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        // TODO(b/274157614): Export this to framework when we can access hidden
-                        //  APIs.
-                        // @exportToFramework:startStrip()
-                        // TODO(b/274157614) start exporting this when it is unhidden in framework
-                        .setDeletionPropagation(true)
-                        // @exportToFramework:endStrip()
                         .build()
                 ).build();
 
         JoinableConfig joinableConfig = JoinableConfig.newBuilder()
                 .setValueType(JoinableConfig.ValueType.Code.QUALIFIED_ID)
-                // @exportToFramework:startStrip()
-                .setPropagateDelete(true)
-                // @exportToFramework:endStrip()
                 .build();
 
         SchemaTypeConfigProto expectedAlbumProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("Album")
+                .setDescription("")
                 .setVersion(0)
                 .addProperties(
                         PropertyConfigProto.newBuilder()
                                 .setPropertyName("artist")
+                                .setDescription("")
                                 .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                 .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                                 .setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -176,6 +298,7 @@
 
         SchemaTypeConfigProto expectedSchemaProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("EmailMessage")
+                .setDescription("")
                 .setVersion(12345)
                 .addParentTypes("Email")
                 .addParentTypes("Message")
@@ -212,10 +335,12 @@
 
         SchemaTypeConfigProto expectedPersonProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("Person")
+                .setDescription("")
                 .setVersion(0)
                 .addProperties(
                         PropertyConfigProto.newBuilder()
                                 .setPropertyName("name")
+                                .setDescription("")
                                 .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                 .setCardinality(PropertyConfigProto.Cardinality.Code.REQUIRED)
                                 .setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -225,6 +350,7 @@
                 .addProperties(
                         PropertyConfigProto.newBuilder()
                                 .setPropertyName("worksFor")
+                                .setDescription("")
                                 .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
                                 .setSchemaType("Organization")
                                 .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -236,4 +362,89 @@
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedPersonProto))
                 .isEqualTo(personSchema);
     }
+
+    @Test
+    public void testGetProto_EmbeddingProperty() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(
+                        new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+                                .setCardinality(
+                                        AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .setIndexingType(
+                                        AppSearchSchema.EmbeddingPropertyConfig
+                                                .INDEXING_TYPE_NONE)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.EmbeddingPropertyConfig.Builder("indexableEmbedding")
+                                .setCardinality(
+                                        AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .setIndexingType(
+                                        AppSearchSchema.EmbeddingPropertyConfig
+                                                .INDEXING_TYPE_SIMILARITY)
+                                .build())
+                .build();
+
+        SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("Email")
+                .setDescription("")
+                .setVersion(12345)
+                .addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("subject")
+                        .setDescription("")
+                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setStringIndexingConfig(
+                                StringIndexingConfig.newBuilder()
+                                        .setTokenizerType(
+                                                StringIndexingConfig.TokenizerType.Code.PLAIN)
+                                        .setTermMatchType(TermMatchType.Code.PREFIX)
+                        )
+                ).addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("body")
+                        .setDescription("")
+                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setStringIndexingConfig(
+                                StringIndexingConfig.newBuilder()
+                                        .setTokenizerType(
+                                                StringIndexingConfig.TokenizerType.Code.PLAIN)
+                                        .setTermMatchType(TermMatchType.Code.PREFIX)
+                        )
+                ).addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("embedding")
+                        .setDescription("")
+                        .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                ).addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("indexableEmbedding")
+                        .setDescription("")
+                        .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setEmbeddingIndexingConfig(
+                                EmbeddingIndexingConfig.newBuilder()
+                                        .setEmbeddingIndexingType(
+                                                EmbeddingIndexingConfig.EmbeddingIndexingType.Code
+                                                        .LINEAR_SEARCH)
+                        )
+                ).build();
+
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/12345))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
index ccf3b51..9ce2a32 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -27,6 +27,7 @@
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
@@ -84,7 +85,7 @@
         removePrefixesFromDocument(documentProtoBuilder);
         removePrefixesFromDocument(joinedDocProtoBuilder);
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
-                searchResultProto, schemaMap, config);
+                searchResultProto, new SchemaCache(schemaMap), config);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult result = searchResultPage.getResults().get(0);
         assertThat(result.getPackageName()).isEqualTo("com.package.foo");
@@ -148,7 +149,8 @@
 
         removePrefixesFromDocument(documentProtoBuilder);
         Exception e = assertThrows(AppSearchException.class,
-                () -> SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap,
+                () -> SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
+                        new SchemaCache(schemaMap),
                         new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                                 new LocalStorageIcingOptionsConfig())));
         assertThat(e.getMessage())
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 778ab7a..dbb8f58 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
@@ -30,6 +30,7 @@
 import androidx.appsearch.localstorage.IcingOptionsConfig;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
 import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
@@ -78,8 +79,8 @@
                         mLocalStorageIcingOptionsConfig
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
@@ -105,13 +106,13 @@
                 prefix2, ImmutableSet.of(
                         prefix2 + "namespace1",
                         prefix2 + "namespace2")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        prefix1 + "typeA", configProto,
-                        prefix1 + "typeB", configProto),
-                prefix2, ImmutableMap.of(
-                        prefix2 + "typeA", configProto,
-                        prefix2 + "typeB", configProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                prefix1 + "typeA", configProto,
+                                prefix1 + "typeB", configProto),
+                        prefix2, ImmutableMap.of(
+                                prefix2 + "typeA", configProto,
+                                prefix2 + "typeB", configProto))),
                 mLocalStorageIcingOptionsConfig);
         // Convert SearchSpec to proto.
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -155,13 +156,13 @@
                 prefix2, ImmutableSet.of(
                         prefix2 + "namespace1",
                         prefix2 + "namespace2")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        prefix1 + "typeA", configProto,
-                        prefix1 + "typeB", configProto),
-                prefix2, ImmutableMap.of(
-                        prefix2 + "typeA", configProto,
-                        prefix2 + "typeB", configProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                prefix1 + "typeA", configProto,
+                                prefix1 + "typeB", configProto),
+                        prefix2, ImmutableMap.of(
+                                prefix2 + "typeA", configProto,
+                                prefix2 + "typeB", configProto))),
                 mLocalStorageIcingOptionsConfig);
 
         // Convert SearchSpec to proto.
@@ -227,15 +228,15 @@
                         prefix2,
                         ImmutableSet.of(
                                 prefix2 + "namespace1", prefix2 + "namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                        prefix1,
-                        ImmutableMap.of(
-                                prefix1 + "typeA", configProto,
-                                prefix1 + "typeB", configProto),
-                        prefix2,
-                        ImmutableMap.of(
-                                prefix2 + "typeA", configProto,
-                                prefix2 + "typeB", configProto)),
+                        new SchemaCache(/*schemaMap=*/ ImmutableMap.of(
+                                prefix1,
+                                ImmutableMap.of(
+                                        prefix1 + "typeA", configProto,
+                                        prefix1 + "typeB", configProto),
+                                prefix2,
+                                ImmutableMap.of(
+                                        prefix2 + "typeA", configProto,
+                                        prefix2 + "typeB", configProto))),
                         mLocalStorageIcingOptionsConfig);
 
         VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
@@ -287,8 +288,9 @@
                 /*queryExpression=*/"",
                 searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
                 /*namespaceMap=*/ImmutableMap.of(prefix, ImmutableSet.of(prefix + namespace)),
-                /*schemaMap=*/ImmutableMap.of(prefix, ImmutableMap.of(prefix + schemaType,
-                SchemaTypeConfigProto.getDefaultInstance())),
+                new SchemaCache(/*schemaMap=*/
+                        ImmutableMap.of(prefix, ImmutableMap.of(prefix + schemaType,
+                                SchemaTypeConfigProto.getDefaultInstance()))),
                 mLocalStorageIcingOptionsConfig).toScoringSpecProto();
         TypePropertyWeights typePropertyWeights = TypePropertyWeights.newBuilder()
                 .setSchemaType(prefix + schemaType)
@@ -317,7 +319,7 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig).toScoringSpecProto();
 
         assertThat(scoringSpecProto.getOrderBy().getNumber())
@@ -342,11 +344,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = convert.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
         assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -380,12 +382,12 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
         assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -428,12 +430,12 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 namespaceMap,
-                schemaMap);
+                new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(1);
         assertThat(resultSpecProto.getResultGroupings(0).getEntryGroupings(0).getNamespace())
@@ -479,12 +481,12 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 namespaceMap,
-                schemaMap);
+                new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -507,7 +509,7 @@
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
 
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
                 .build();
 
         SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -515,12 +517,12 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -534,7 +536,7 @@
     @Test
     public void testToResultSpecProto_projectionNoPrefixes_withWildcard() {
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
                 .build();
 
         SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -542,12 +544,12 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
@@ -555,8 +557,59 @@
         assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/274157614): Export this to framework when property filters are made public
+    @Test
+    public void testToResultSpecProto_projection_removeSchemaWithoutParentInFilter() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("Person")
+                .addProjection("Artist", ImmutableList.of("name"))
+                .addProjection("Other", ImmutableList.of("email"))
+                .build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Other")
+                        .build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/Person", personSchema,
+                        "package$database/Artist", artistSchema,
+                        "package$database/Other", otherSchema));
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix, ImmutableSet.of("package$database/namespace"));
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"",
+                searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/namespaceMap,
+                new SchemaCache(schemaMap),
+                mLocalStorageIcingOptionsConfig);
+
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+                namespaceMap,
+                new SchemaCache(schemaMap));
+
+        // The "name" property specified in Artist's projection should remain in the result,
+        // since even though Artist doesn't exist in the original schema filters directly, we have
+        // specified its parent, Person, in the schema filters.
+        // The "email" property specified in Other's projection should be dropped as usual.
+        assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType()).isEqualTo(
+                "package$database/Artist");
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getPathsCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
+    }
+
     @Test
     public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -591,7 +644,7 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -609,7 +662,6 @@
         assertThat(nestedSearchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("type");
     }
 
-    // TODO(b/274157614): Export this to framework when property filters are made public
     @Test
     public void testToSearchSpecProto_propertyFilter_withWildcard() {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -624,7 +676,7 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -635,7 +687,57 @@
         assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
     }
 
-    // @exportToFramework:endStrip()
+    @Test
+    public void testToSearchSpecProto_propertyFilter_removeSchemaWithoutParentInFilter() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("Person")
+                .addFilterProperties("Artist", ImmutableList.of("name"))
+                .addFilterProperties("Other", ImmutableList.of("email"))
+                .build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Other")
+                        .build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/Person", personSchema,
+                        "package$database/Artist", artistSchema,
+                        "package$database/Other", otherSchema));
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix, ImmutableSet.of("package$database/namespace"));
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"",
+                searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/namespaceMap,
+                new SchemaCache(schemaMap),
+                mLocalStorageIcingOptionsConfig);
+
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+        // The "name" property specified in Artist's property filters should remain in the result,
+        // since even though Artist doesn't exist in the original schema filters directly, we have
+        // specified its parent, Person, in the schema filters.
+        // The "email" property specified in Other's property filters should be dropped as usual.
+        assertThat(searchSpecProto.getTypePropertyFiltersCount()).isEqualTo(1);
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getSchemaType()).isEqualTo(
+                "package$database/Artist");
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getPathsCount()).isEqualTo(1);
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
+    }
+
     @Test
     public void testToResultSpecProto_weight_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -671,7 +773,7 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(personPrefix, actionPrefix),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
 
         ScoringSpecProto scoringSpecProto = converter.toScoringSpecProto();
@@ -709,7 +811,7 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(
@@ -719,7 +821,7 @@
                         prefix2, ImmutableSet.of(
                                 prefix2 + "namespaceA",
                                 prefix2 + "namespaceB")),
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have same package name.
@@ -762,11 +864,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 namespaceMap,
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 namespaceMap,
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have same namespace.
@@ -786,8 +888,6 @@
                         PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getNamespace()));
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/258715421) start exporting this when it is unhidden in framework
     @Test
     public void testToResultSpecProto_groupBySchema() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
@@ -811,11 +911,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 /*namespaceMap=*/ImmutableMap.of(),
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                schemaMap);
+                new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have the same schema type.
@@ -834,7 +934,6 @@
                 .isEqualTo(
                     PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getSchema()));
     }
-    // @exportToFramework:endStrip()
 
     @Test
     public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
@@ -857,11 +956,12 @@
                 /*queryExpression=*/"query",
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
-                namespaceMap, /*schemaMap=*/ImmutableMap.of(),
+                namespaceMap,
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 namespaceMap,
-                /*schemaMap=*/ImmutableMap.of());
+                new SchemaCache());
 
         // All namespace should be separated.
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -871,8 +971,6 @@
         assertThat(resultSpecProto.getResultGroupings(3).getEntryGroupingsList()).hasSize(1);
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/258715421) start exporting this when it is unhidden in framework
     @Test
     public void testToResultSpecProto_groupBySchemaAndPackage() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
@@ -896,11 +994,11 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 /*namespaceMap=*/ImmutableMap.of(),
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto = converter.toResultSpecProto(
                 /*namespaceMap=*/ImmutableMap.of(),
-                schemaMap);
+                new SchemaCache(schemaMap));
 
         // All schema should be separated.
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -940,9 +1038,10 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap,
+                new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
         ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1032,9 +1131,10 @@
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 namespaceMap,
-                schemaMap,
+                new SchemaCache(schemaMap),
                 mLocalStorageIcingOptionsConfig);
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap,
+                new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(8);
         ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1110,7 +1210,6 @@
         assertThat(grouping8.getEntryGroupings(0).getSchema())
                 .isEqualTo("package1$database/typeB");
     }
-    // @exportToFramework:endStrip()
 
     @Test
     public void testGetTargetNamespaceFilters_emptySearchingFilter() {
@@ -1127,7 +1226,8 @@
                 /*queryExpression=*/"",
                 searchSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
-                namespaceMap, /*schemaMap=*/ImmutableMap.of(),
+                namespaceMap,
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1153,7 +1253,7 @@
                         "package$database1/namespace2"),
                 prefix2, ImmutableSet.of("package$database2/namespace3",
                         "package$database2/namespace4")),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1176,7 +1276,7 @@
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1",
                         "package$database1/namespace2")),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1200,7 +1300,7 @@
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1",
                         "package$database1/namespace2")),
-                /*schemaMap=*/ImmutableMap.of(),
+                new SchemaCache(),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1222,13 +1322,13 @@
                 /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        "package$database1/typeA", schemaTypeConfigProto,
-                        "package$database1/typeB", schemaTypeConfigProto),
-                prefix2, ImmutableMap.of(
-                        "package$database2/typeC", schemaTypeConfigProto,
-                        "package$database2/typeD", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto),
+                        prefix2, ImmutableMap.of(
+                                "package$database2/typeC", schemaTypeConfigProto,
+                                "package$database2/typeD", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Empty searching filter will get all types for target filter
@@ -1251,13 +1351,13 @@
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        "package$database1/typeA", schemaTypeConfigProto,
-                        "package$database1/typeB", schemaTypeConfigProto),
-                prefix2, ImmutableMap.of(
-                        "package$database2/typeC", schemaTypeConfigProto,
-                        "package$database2/typeD", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto),
+                        prefix2, ImmutableMap.of(
+                                "package$database2/typeC", schemaTypeConfigProto,
+                                "package$database2/typeD", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Only search prefix1 will return typeA and B.
@@ -1279,10 +1379,10 @@
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        "package$database1/typeA", schemaTypeConfigProto,
-                        "package$database1/typeB", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching schema filter is not empty, the target schema filter will be the
@@ -1293,6 +1393,96 @@
     }
 
     @Test
+    public void testGetTargetSchemaFilters_polymorphismExpansion() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("Person", "nonExist").build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Other")
+                        .build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/Person", personSchema,
+                        "package$database/Artist", artistSchema,
+                        "package$database/Other", otherSchema));
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"",
+                searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/ImmutableMap.of(
+                prefix, ImmutableSet.of("package$database/namespace")),
+                new SchemaCache(schemaMap),
+                mLocalStorageIcingOptionsConfig);
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+        // The schema filter of "Person" specified in searchSpec will be expanded to "Artist" via
+        // polymorphism.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database/Person", "package$database/Artist");
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_polymorphismExpansion_multipleLevel() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("A", "B").build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto schemaA =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaB =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/B")
+                        .build();
+        SchemaTypeConfigProto schemaC =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/C")
+                        .addParentTypes("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaD =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/D")
+                        .addParentTypes("package$database/C")
+                        .build();
+        SchemaTypeConfigProto schemaE =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/E")
+                        .addParentTypes("package$database/B")
+                        .addParentTypes("package$database/C")
+                        .build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/A", schemaA,
+                        "package$database/B", schemaB,
+                        "package$database/C", schemaC,
+                        "package$database/D", schemaD,
+                        "package$database/E", schemaE));
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
+                /*queryExpression=*/"",
+                searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/ImmutableMap.of(
+                prefix, ImmutableSet.of("package$database/namespace")),
+                new SchemaCache(schemaMap),
+                mLocalStorageIcingOptionsConfig);
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database/A", "package$database/B", "package$database/C",
+                "package$database/D", "package$database/E");
+    }
+
+    @Test
     public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
         // Put non-exist searching schema.
         SearchSpec searchSpec = new SearchSpec.Builder()
@@ -1306,10 +1496,10 @@
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        "package$database1/typeA", schemaTypeConfigProto,
-                        "package$database1/typeB", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If there is no intersection of the schema filters that user want to search over and
@@ -1335,11 +1525,11 @@
                 /*prefixes=*/ImmutableSet.of(prefix),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix, ImmutableSet.of("package$database/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix, ImmutableMap.of(
-                        "package$database/schema1", schemaTypeConfigProto,
-                        "package$database/schema2", schemaTypeConfigProto,
-                        "package$database/schema3", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix, ImmutableMap.of(
+                                "package$database/schema1", schemaTypeConfigProto,
+                                "package$database/schema2", schemaTypeConfigProto,
+                                "package$database/schema3", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
 
         converter.removeInaccessibleSchemaFilter(
@@ -1386,7 +1576,7 @@
                         /*queryExpression=*/"",
                         searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
                         /*namespaceMap=*/namespaceMap,
-                        /*schemaMap=*/ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         assertThat(emptySchemaConverter.hasNothingToSearch()).isTrue();
 
@@ -1395,7 +1585,7 @@
                         /*queryExpression=*/"",
                         searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
                         /*namespaceMap=*/ImmutableMap.of(),
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         assertThat(emptyNamespaceConverter.hasNothingToSearch()).isTrue();
 
@@ -1403,7 +1593,8 @@
                 new SearchSpecToProtoConverter(
                         /*queryExpression=*/"",
                         searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
-                        namespaceMap, schemaMap,
+                        namespaceMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         assertThat(nonEmptyConverter.hasNothingToSearch()).isFalse();
 
@@ -1437,11 +1628,11 @@
                 /*prefixes=*/ImmutableSet.of(prefix),
                 /*namespaceMap=*/ImmutableMap.of(
                 prefix, ImmutableSet.of("package$database/namespace1")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix, ImmutableMap.of(
-                        "package$database/schema1", schemaTypeConfigProto,
-                        "package$database/schema2", schemaTypeConfigProto,
-                        "package$database/schema3", schemaTypeConfigProto)),
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix, ImmutableMap.of(
+                                "package$database/schema1", schemaTypeConfigProto,
+                                "package$database/schema2", schemaTypeConfigProto,
+                                "package$database/schema3", schemaTypeConfigProto))),
                 mLocalStorageIcingOptionsConfig);
 
         converter.removeInaccessibleSchemaFilter(
@@ -1489,7 +1680,7 @@
                         /*queryExpression=*/"",
                         searchSpec, /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        schemaTypeMap,
+                        new SchemaCache(schemaTypeMap),
                         mLocalStorageIcingOptionsConfig);
 
         TypePropertyWeights expectedTypePropertyWeight1 =
@@ -1538,9 +1729,9 @@
                         /*namespaceMap=*/ImmutableMap.of(
                         prefix1,
                         ImmutableSet.of(prefix1 + "namespace1")),
-                        /*schemaMap=*/ImmutableMap.of(
-                        prefix1,
-                        ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)),
+                        new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                                prefix1,
+                                ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
 
         ScoringSpecProto convertedScoringSpecProto = converter.toScoringSpecProto();
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
index f918784..365d403 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.NamespaceDocumentUriGroup;
@@ -54,10 +55,10 @@
                 prefix1, ImmutableSet.of(
                         prefix1 + "namespace1",
                         prefix1 + "namespace2")),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        prefix1 + "typeA", configProto,
-                        prefix1 + "typeB", configProto)));
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                prefix1 + "typeA", configProto,
+                                prefix1 + "typeB", configProto))));
 
         SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
 
@@ -74,9 +75,6 @@
                         .build());
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/230553264) remove this when it is deprecated and replaced by
-    //  advanced query property filters or it is exportable.
     @Test
     public void testToProto_propertyFilters() throws Exception {
         SearchSuggestionSpec searchSuggestionSpec =
@@ -91,10 +89,10 @@
                 searchSuggestionSpec,
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of(
-                prefix1, ImmutableMap.of(
-                        prefix1 + "typeA", configProto,
-                        prefix1 + "typeB", configProto)));
+                new SchemaCache(/*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                prefix1 + "typeA", configProto,
+                                prefix1 + "typeB", configProto))));
 
         SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
         assertThat(proto.getTypePropertyFiltersList()).containsExactly(
@@ -103,5 +101,4 @@
                         .addPaths("property1").addPaths("property2")
                         .build());
     }
-    // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index fbc9a93..ad86e02 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -23,6 +23,7 @@
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.UnlimitedLimitConfig;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 
@@ -95,7 +96,7 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -136,7 +137,7 @@
 
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
@@ -193,7 +194,7 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -280,7 +281,7 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                SCHEMA_MAP, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                new SchemaCache(SCHEMA_MAP), new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
index 667dd86..c86d4cc 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -227,6 +227,7 @@
         int nativeLockAcquisitionLatencyMillis = 20;
         int javaToNativeJniLatencyMillis = 21;
         int nativeToJavaJniLatencyMillis = 22;
+        String searchSourceLogTag = "tag";
         final SearchStats.Builder sStatsBuilder = new SearchStats.Builder(visibilityScope,
                 TEST_PACKAGE_NAME)
                 .setDatabase(TEST_DATA_BASE)
@@ -253,7 +254,8 @@
                 .setDocumentRetrievingLatencyMillis(nativeDocumentRetrievingLatencyMillis)
                 .setNativeLockAcquisitionLatencyMillis(nativeLockAcquisitionLatencyMillis)
                 .setJavaToNativeJniLatencyMillis(javaToNativeJniLatencyMillis)
-                .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis);
+                .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis)
+                .setSearchSourceLogTag(searchSourceLogTag);
         final SearchStats sStats = sStatsBuilder.build();
 
         assertThat(sStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
@@ -295,7 +297,7 @@
                 javaToNativeJniLatencyMillis);
         assertThat(sStats.getNativeToJavaJniLatencyMillis()).isEqualTo(
                 nativeToJavaJniLatencyMillis);
-
+        assertThat(sStats.getSearchSourceLogTag()).isEqualTo(searchSourceLogTag);
     }
 
     @Test
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
new file mode 100644
index 0000000..f8a941d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ClickStatsTest {
+    @Test
+    public void testBuilder() {
+        long timestampMillis = 1L;
+        long timeStayOnResultMillis = 2L;
+        int resultRankInBlock = 3;
+        int resultRankGlobal = 4;
+        boolean isGoodClick = false;
+
+        final ClickStats clickStats =
+                new ClickStats.Builder()
+                        .setTimestampMillis(timestampMillis)
+                        .setTimeStayOnResultMillis(timeStayOnResultMillis)
+                        .setResultRankInBlock(resultRankInBlock)
+                        .setResultRankGlobal(resultRankGlobal)
+                        .setIsGoodClick(isGoodClick)
+                        .build();
+
+        assertThat(clickStats.getTimestampMillis()).isEqualTo(timestampMillis);
+        assertThat(clickStats.getTimeStayOnResultMillis()).isEqualTo(timeStayOnResultMillis);
+        assertThat(clickStats.getResultRankInBlock()).isEqualTo(resultRankInBlock);
+        assertThat(clickStats.getResultRankGlobal()).isEqualTo(resultRankGlobal);
+        assertThat(clickStats.isGoodClick()).isEqualTo(isGoodClick);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
new file mode 100644
index 0000000..66e2ed4
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchIntentStatsTest {
+    static final String TEST_PACKAGE_NAME = "package.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+
+    @Test
+    public void testBuilder() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1L;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+
+        assertThat(searchIntentStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats.getClicksStats()).containsExactly(clickStats0, clickStats1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1L;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(searchIntentStats0).build();
+
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats()).containsExactly(clickStats0, clickStats1);
+    }
+
+    @Test
+    public void testBuilderCopy_copiedFieldsCanBeUpdated() {
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+
+        // Build another SearchIntentStats based on the previous one, with fields changed.
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(searchIntentStats0)
+                        .setDatabase("database2")
+                        .setPrevQuery("query3")
+                        .setCurrQuery("query4")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(clickStats2)
+                        .build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo("query1");
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("query2");
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1L);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(2);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo("database2");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("query3");
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("query4");
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2L);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(4);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+
+    @Test
+    public void testBuilder_addClicksStats_byCollection() {
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+        Set<ClickStats> clicksStats = ImmutableSet.of(clickStats0, clickStats1);
+
+        final SearchIntentStats searchIntentStats =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addClicksStats(clicksStats)
+                        .build();
+
+        assertThat(searchIntentStats.getClicksStats()).containsExactlyElementsIn(clicksStats);
+    }
+
+    @Test
+    public void testBuilder_builderReuse() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        SearchIntentStats.Builder builder =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1);
+
+        final SearchIntentStats searchIntentStats0 = builder.build();
+
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        builder.addClicksStats(clickStats2);
+
+        final SearchIntentStats searchIntentStats1 = builder.build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+
+    @Test
+    public void testBuilder_builderReuse_byCollection() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        SearchIntentStats.Builder builder =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(ImmutableSet.of(clickStats0, clickStats1));
+
+        final SearchIntentStats searchIntentStats0 = builder.build();
+
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        builder.addClicksStats(ImmutableSet.of(clickStats2));
+
+        final SearchIntentStats searchIntentStats1 = builder.build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java
new file mode 100644
index 0000000..b287769
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchSessionStatsTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchSessionStatsTest {
+    static final String TEST_PACKAGE_NAME = "package.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+
+    @Test
+    public void testBuilder() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+                        .build();
+
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+    }
+
+    @Test
+    public void testBuilder_addSearchIntentsStats_byCollection() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        Set<SearchIntentStats> searchIntentsStats =
+                ImmutableSet.of(searchIntentStats0, searchIntentStats1);
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentsStats)
+                        .build();
+
+        assertThat(searchSessionStats.getSearchIntentsStats())
+                .containsExactlyElementsIn(searchIntentsStats);
+    }
+
+    @Test
+    public void testBuilder_builderReuse() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        SearchSessionStats.Builder builder =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1);
+
+        final SearchSessionStats searchSessionStats0 = builder.build();
+
+        final SearchIntentStats searchIntentStats2 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query2")
+                        .setCurrQuery("query3")
+                        .setTimestampMillis(3L)
+                        .setNumResultsFetched(6)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(13L)
+                                        .setTimeStayOnResultMillis(23L)
+                                        .setResultRankInBlock(33)
+                                        .setResultRankGlobal(43)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        builder.addSearchIntentsStats(searchIntentStats2);
+
+        final SearchSessionStats searchSessionStats1 = builder.build();
+
+        // Check that searchSessionStats0 wasn't altered.
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+
+        // Check that searchSessionStats1 has the new values.
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+    }
+
+    @Test
+    public void testBuilder_builderReuse_byCollection() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        SearchSessionStats.Builder builder =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(
+                                ImmutableSet.of(searchIntentStats0, searchIntentStats1));
+
+        final SearchSessionStats searchSessionStats0 = builder.build();
+
+        final SearchIntentStats searchIntentStats2 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query2")
+                        .setCurrQuery("query3")
+                        .setTimestampMillis(3L)
+                        .setNumResultsFetched(6)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(13L)
+                                        .setTimeStayOnResultMillis(23L)
+                                        .setResultRankInBlock(33)
+                                        .setResultRankGlobal(43)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        builder.addSearchIntentsStats(ImmutableSet.of(searchIntentStats2));
+
+        final SearchSessionStats searchSessionStats1 = builder.build();
+
+        // Check that searchSessionStats0 wasn't altered.
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+
+        // Check that searchSessionStats1 has the new values.
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+    }
+
+    @Test
+    public void testGetEndSessionSearchIntentStats() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+                        .build();
+
+        SearchIntentStats endSessionSearchIntentStats =
+                searchSessionStats.getEndSessionSearchIntentStats();
+        // End session SearchIntentStats should be identical to the last added SearchIntentStats,
+        // except the previous query is null and query correction type is
+        // QUERY_CORRECTION_TYPE_END_SESSION.
+        assertThat(endSessionSearchIntentStats).isNotNull();
+        assertThat(endSessionSearchIntentStats.getPackageName())
+                .isEqualTo(searchIntentStats1.getPackageName());
+        assertThat(endSessionSearchIntentStats.getDatabase())
+                .isEqualTo(searchIntentStats1.getDatabase());
+        assertThat(endSessionSearchIntentStats.getCurrQuery())
+                .isEqualTo(searchIntentStats1.getCurrQuery());
+        assertThat(endSessionSearchIntentStats.getTimestampMillis())
+                .isEqualTo(searchIntentStats1.getTimestampMillis());
+        assertThat(endSessionSearchIntentStats.getNumResultsFetched())
+                .isEqualTo(searchIntentStats1.getNumResultsFetched());
+        assertThat(endSessionSearchIntentStats.getClicksStats())
+                .containsExactlyElementsIn(searchIntentStats1.getClicksStats());
+
+        assertThat(endSessionSearchIntentStats.getPrevQuery()).isNull();
+        assertThat(endSessionSearchIntentStats.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION);
+    }
+
+    @Test
+    public void testGetEndSessionSearchIntentStats_emptySearchIntentsShouldReturnNull() {
+        // Create a SearchSessionStats without search intents.
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .build();
+
+        assertThat(searchSessionStats.getEndSessionSearchIntentStats()).isNull();
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java
new file mode 100644
index 0000000..36e0ca0
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+
+import org.junit.Test;
+
+public class ClickActionGenericDocumentTest {
+    @Test
+    public void testBuild() {
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("body")
+                        .setResultRankInBlock(12)
+                        .setResultRankGlobal(34)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+
+        assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+        assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+        assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(clickActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+        assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+        assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+    }
+
+    @Test
+    public void testBuild_fromGenericDocument() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyLong("resultRankInBlock", 12)
+                        .setPropertyLong("resultRankGlobal", 34)
+                        .setPropertyLong("timeStayOnResultMillis", 2000)
+                        .build();
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder(document).build();
+
+        assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+        assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+        assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(clickActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+        assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+        assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testBuild_fromDocumentClass() throws Exception {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */1000)
+                        .setQuery("body")
+                        .setReferencedQualifiedId("pkg$db/ns#doc")
+                        .setResultRankInBlock(12)
+                        .setResultRankGlobal(34)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder(
+                        GenericDocument.fromDocumentClass(clickAction)).build();
+
+        assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+        assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+        assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(clickActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+        assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+        assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testBuild_invalidActionTypeThrowsException() {
+        GenericDocument documentWithoutActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")
+                        .build();
+        IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,
+                () -> new ClickActionGenericDocument.Builder(documentWithoutActionType));
+        assertThat(e1.getMessage())
+                .isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+        GenericDocument documentWithUnknownActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+                        .build();
+        IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,
+                () -> new ClickActionGenericDocument.Builder(documentWithUnknownActionType));
+        assertThat(e2.getMessage())
+                .isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+        GenericDocument documentWithIncorrectActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+                        .build();
+        IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,
+                () -> new ClickActionGenericDocument.Builder(documentWithIncorrectActionType));
+        assertThat(e3.getMessage())
+                .isEqualTo("Invalid action type for ClickActionGenericDocument");
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
new file mode 100644
index 0000000..ea705b7
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+
+import org.junit.Test;
+
+public class SearchActionGenericDocumentTest {
+    @Test
+    public void testBuild() {
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("body")
+                        .setFetchedResultCount(123)
+                        .build();
+
+        assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+        assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+        assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(searchActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+    }
+
+    @Test
+    public void testBuild_fromGenericDocument() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .setPropertyLong("fetchedResultCount", 123)
+                        .build();
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument(document);
+
+        assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+        assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+        assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(searchActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testBuild_fromDocumentClass() throws Exception {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+                        .setQuery("body")
+                        .setFetchedResultCount(123)
+                        .build();
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument(GenericDocument.fromDocumentClass(searchAction));
+
+        assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+        assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+        assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(searchActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testBuild_invalidActionTypeThrowsException() {
+        GenericDocument documentWithoutActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .build();
+        IllegalArgumentException e1 = assertThrows(IllegalArgumentException.class,
+                () -> new SearchActionGenericDocument.Builder(documentWithoutActionType));
+        assertThat(e1.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+        GenericDocument documentWithUnknownActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+                        .build();
+        IllegalArgumentException e2 = assertThrows(IllegalArgumentException.class,
+                () -> new SearchActionGenericDocument.Builder(documentWithUnknownActionType));
+        assertThat(e2.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+        GenericDocument documentWithIncorrectActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+                        .build();
+        IllegalArgumentException e3 = assertThrows(IllegalArgumentException.class,
+                () -> new SearchActionGenericDocument.Builder(documentWithIncorrectActionType));
+        assertThat(e3.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
new file mode 100644
index 0000000..24b59a6
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
@@ -0,0 +1,1349 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchSessionStatsExtractorTest {
+    private static final String TEST_PACKAGE_NAME = "test.package.name";
+    private static final String TEST_DATABASE = "database";
+
+    @Test
+    public void testExtract() {
+        // Create search action and click action generic documents.
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(5000)
+                        .setQuery("test")
+                        .setFetchedResultCount(10)
+                        .build();
+        GenericDocument clickAction3 =
+                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(6000)
+                        .setQuery("test")
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(4)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc3")
+                        .build();
+        GenericDocument clickAction4 =
+                new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7000)
+                        .setQuery("test")
+                        .setResultRankInBlock(4)
+                        .setResultRankGlobal(8)
+                        .setTimeStayOnResultMillis(256)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc4")
+                        .build();
+        GenericDocument clickAction5 =
+                new ClickActionGenericDocument.Builder("namespace", "click5", "builtin:ClickAction")
+                        .setCreationTimestampMillis(8000)
+                        .setQuery("test")
+                        .setResultRankInBlock(6)
+                        .setResultRankGlobal(12)
+                        .setTimeStayOnResultMillis(2048)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc5")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1,
+                        clickAction1,
+                        clickAction2,
+                        searchAction2,
+                        clickAction3,
+                        clickAction4,
+                        clickAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).hasSize(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+        assertThat(searchIntentStats0.getClicksStats().get(1).isGoodClick()).isFalse();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(5000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).hasSize(3);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats1.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(256);
+        assertThat(searchIntentStats1.getClicksStats().get(1).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(2048);
+        assertThat(searchIntentStats1.getClicksStats().get(2).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_noSearchActionShouldReturnEmptyList() {
+        // Create search action and click action generic documents.
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void testExtract_shouldSkipUnknownActionTypeDocuments() {
+        // Create search action and click action generic documents.
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyString("query", "tes")
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .setPropertyLong("resultRankInBlock", 1)
+                        .setPropertyLong("resultRankGlobal", 2)
+                        .setPropertyLong("timeStayOnResultMillis", 512)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+
+        // Since clickAction1 doesn't have property "actionType", it should be skipped without
+        // throwing any exception.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).hasSize(1);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(3000);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(3);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(6);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+        assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testExtract_builtFromDocumentClass() throws Exception {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .setQuery("tes")
+                        .setReferencedQualifiedId("pkg$db/ns#doc1")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .setQuery("tes")
+                        .setReferencedQualifiedId("pkg$db/ns#doc2")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+        SearchAction searchAction2 =
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */5000)
+                        .setQuery("test")
+                        .setFetchedResultCount(10)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */6000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc3")
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(4)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction4 =
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */7000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc4")
+                        .setResultRankInBlock(4)
+                        .setResultRankGlobal(8)
+                        .setTimeStayOnResultMillis(256)
+                        .build();
+        ClickAction clickAction5 =
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */8000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc5")
+                        .setResultRankInBlock(6)
+                        .setResultRankGlobal(12)
+                        .setTimeStayOnResultMillis(2048)
+                        .build();
+
+        // Use PutDocumentsRequest taken action API to convert document class to GenericDocument.
+        PutDocumentsRequest putDocumentsRequest = new PutDocumentsRequest.Builder()
+                .addTakenActions(searchAction1, clickAction1, clickAction2,
+                        searchAction2, clickAction3, clickAction4, clickAction5)
+                .build();
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE,
+                                putDocumentsRequest.getTakenActionGenericDocuments());
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).hasSize(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+        assertThat(searchIntentStats0.getClicksStats().get(1).isGoodClick()).isFalse();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(5000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).hasSize(3);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats1.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(256);
+        assertThat(searchIntentStats1.getClicksStats().get(1).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(2048);
+        assertThat(searchIntentStats1.getClicksStats().get(2).isGoodClick()).isTrue();
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testExtract_detectAndSkipSearchNoise_appendNewCharacters() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction5 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search5", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("testing")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2, searchAction3 should be considered as noise since they're intermediate
+        // search actions with no clicks. The extractor should create search intents only for the
+        // others.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_detectAndSkipSearchNoise_deleteCharacters() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("testing")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction5 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search5", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2, searchAction3 should be considered as noise since they're intermediate
+        // search actions with no clicks. The extractor should create search intents only for the
+        // others.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_occursAfterThresholdShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2 should not be considered as noise since it occurs after the threshold from
+        // searchAction1 (and therefore not intermediate search actions).
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_nonPrefixQueryStringShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("apple")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1500)
+                        .setQuery("application")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("email")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("google")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3, searchAction4);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(4);
+
+        // searchAction2 and searchAction3 should not be considered as noise since neither query
+        // string is a prefix of the previous one (and therefore not intermediate search actions).
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("apple");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(1500);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("application");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("apple");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("email");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("application");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 3
+        SearchIntentStats searchIntentStats3 = searchSessionStats0.getSearchIntentsStats().get(3);
+        assertThat(searchIntentStats3.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats3.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats3.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats3.getCurrQuery()).isEqualTo("google");
+        assertThat(searchIntentStats3.getPrevQuery()).isEqualTo("email");
+        assertThat(searchIntentStats3.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats3.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats3.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_lastSearchActionShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        // searchAction2 should not be considered as noise since it is the last search action (and
+        // therefore not an intermediate search action).
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_lastSearchActionOfRelatedSearchSequenceShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(602001)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        // searchAction2 should not be considered as noise:
+        // - searchAction3 is independent from searchAction2 and therefore forms an independent
+        //   search session.
+        // - So searchAction2 is the last search action of its search session (and therefore not an
+        // intermediate search action).
+        assertThat(result).hasSize(2);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        SearchSessionStats searchSessionStats1 = result.get(1);
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 1, search intent 0
+        SearchIntentStats searchIntentStats2 = searchSessionStats1.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(602001);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isNull();
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_withClickActionShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(10)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2050)
+                        .setQuery("te")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("test")
+                        .setFetchedResultCount(5)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, clickAction1, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // Even though searchAction2 is an intermediate search action, it should not be considered
+        // as noise since there is at least 1 valid click action associated with it.
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).hasSize(1);
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(5);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_independentSearchIntentShouldStartNewSearchSession() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(601001)
+                        .setQuery("te")
+                        .setFetchedResultCount(10)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        // Since time difference between searchAction1 and searchAction2 exceeds the threshold,
+        // searchAction2 should be considered as an independent search intent and therefore a new
+        // search session stats is created.
+        assertThat(result).hasSize(2);
+
+        // Search session 0
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 1
+        SearchSessionStats searchSessionStats1 = result.get(1);
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+        SearchIntentStats searchIntentStats1 = searchSessionStats1.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(601001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isNull();
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_shouldSetIsGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setTimeStayOnResultMillis(2001)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(4500)
+                        .setTimeStayOnResultMillis(1999)
+                        .build();
+        GenericDocument clickAction3 =
+                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7000)
+                        .setTimeStayOnResultMillis(1)
+                        .build();
+        GenericDocument clickAction4 =
+                new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7500)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, clickAction1, clickAction2, clickAction3, clickAction4);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(4);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(2001);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+        assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1999);
+        assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isFalse();
+
+        assertThat(searchIntentStats.getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(1);
+        assertThat(searchIntentStats.getClicksStats().get(2).isGoodClick()).isFalse();
+
+        assertThat(searchIntentStats.getClicksStats().get(3).getTimeStayOnResultMillis())
+                .isEqualTo(2000);
+        assertThat(searchIntentStats.getClicksStats().get(3).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_unsetTimeStayOnResultShouldBeGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+        assertThat(result).hasSize(1);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(0);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_nonPositiveTimeStayOnResultShouldBeGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setTimeStayOnResultMillis(-1)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setTimeStayOnResultMillis(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(2);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(-1);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+        assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(0);
+        assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_unknown() {
+        SearchActionGenericDocument searchAction =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+        SearchActionGenericDocument searchActionWithNullQueryStr =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .build();
+
+        // Query correction type should be unknown if the current search action's query string is
+        // null.
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchActionWithNullQueryStr,
+                        /* prevSearchAction= */ null))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchActionWithNullQueryStr,
+                        /* prevSearchAction= */ searchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+
+        // Query correction type should be unknown if the previous search action contains null query
+        // string.
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction,
+                        /* prevSearchAction= */ searchActionWithNullQueryStr))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchActionWithNullQueryStr,
+                        /* prevSearchAction= */ searchActionWithNullQueryStr))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_firstQuery() {
+        SearchActionGenericDocument currSearchAction =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        currSearchAction, /* prevSearchAction= */ null))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_refinement() {
+        SearchActionGenericDocument prevSearchAction =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "baseSearch", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        // Append 1 new character should be query refinement.
+        SearchActionGenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("teste")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction1, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Append 2 new characters should be query refinement.
+        SearchActionGenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setQuery("tester")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction2, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Backspace 1 character should be query refinement.
+        SearchActionGenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setQuery("tes")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction3, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Backspace 1 character and append new character(s) should be query refinement.
+        SearchActionGenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search4", "builtin:SearchAction")
+                        .setQuery("tesla")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction4, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_abandonment() {
+        SearchActionGenericDocument prevSearchAction =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "baseSearch", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        // Completely different query should be query abandonment.
+        SearchActionGenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("unit")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction1, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+        // Backspace 2 characters should be query abandonment.
+        SearchActionGenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search2", "builtin:SearchAction")
+                        .setQuery("te")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction2, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+        // Backspace 2 characters and append new character(s) should be query abandonment.
+        SearchActionGenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                        "namespace", "search3", "builtin:SearchAction")
+                        .setQuery("texas")
+                        .build();
+        assertThat(
+                SearchSessionStatsExtractor.getQueryCorrectionType(
+                        /* currSearchAction= */ searchAction3, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
new file mode 100644
index 0000000..e50274b
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+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;
+
+// "V2 schema" refers to V2 of the VisibilityDocument schema, but no Visibility overlay schema
+// present. Simulates backwards compatibility situations.
+public class VisibilityStoreMigrationFromV2Test {
+
+    /**
+     * 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_from2() throws Exception {
+        // As such, we can treat V2 documents as V3 documents when upgrading, but we need to test
+        // this.
+
+        // Values for a "foo" client
+        String packageNameFoo = "packageFoo";
+        byte[] sha256CertFoo = new byte[32];
+        PackageIdentifier packageIdentifierFoo =
+                new PackageIdentifier(packageNameFoo, sha256CertFoo);
+
+        // Values for a "bar" client
+        String packageNameBar = "packageBar";
+        byte[] sha256CertBar = new byte[32];
+        PackageIdentifier packageIdentifierBar =
+                new PackageIdentifier(packageNameBar, sha256CertBar);
+
+        // Create AppSearchImpl with visibility document version 2;
+        AppSearchImpl appSearchImplInV2 = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Erase overlay schemas since it doesn't exist in released V2 schema.
+        InternalSetSchemaResponse internalSetAndroidVSchemaResponse = appSearchImplInV2.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                // no overlay schema
+                ImmutableList.of(),
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ true, // force push the old version into disk
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(internalSetAndroidVSchemaResponse.isSuccess()).isTrue();
+
+        GetSchemaResponse getSchemaResponse = appSearchImplInV2.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+        GetSchemaResponse getAndroidVOverlaySchemaResponse = appSearchImplInV2.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).isEmpty();
+
+        // Build deprecated visibility documents in version 2
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        InternalVisibilityConfig visibilityConfigV2 = new InternalVisibilityConfig.Builder(
+                prefix + "Schema")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(
+                        new PackageIdentifier(packageNameFoo, sha256CertFoo))
+                .addVisibleToPackage(
+                        new PackageIdentifier(packageNameBar, sha256CertBar))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        GenericDocument visibilityDocumentV2 =
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfigV2);
+
+        // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to
+        // directly put old version of VisibilityDocument.
+        InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV2.setSchema(
+                "package",
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Schema").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*schemaVersion=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Put deprecated visibility documents in version 2 to AppSearchImpl
+        appSearchImplInV2.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                visibilityDocumentV2,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/null);
+
+        // Persist to disk and re-open the AppSearchImpl
+        appSearchImplInV2.close();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        appSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Schema",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
+
+        assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+        assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+                .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)));
+
+        // Check that the visibility overlay schema was added.
+        getSchemaResponse = appSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+        getAndroidVOverlaySchemaResponse = appSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+
+        // But no overlay document was created.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                 () -> appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ prefix + "Schema",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found");
+
+        appSearchImpl.close();
+    }
+}
+
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
index 7b03a10..c91f51c 100644
--- 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
@@ -29,8 +29,8 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -82,19 +82,21 @@
         // "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)
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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)
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                 VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
                         "package", "database"),
                 DEPRECATED_VISIBILITY_SCHEMA_TYPE)
@@ -131,34 +133,41 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
-        VisibilityDocument actualDocument1 = new VisibilityDocument.Builder(
+        GenericDocument actualDocument1 =
                 appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Schema1",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        VisibilityDocument actualDocument2 = new VisibilityDocument.Builder(
+                        /*typePropertyPaths=*/ Collections.emptyMap());
+        GenericDocument actualDocument2 =
                 appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Schema2",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
+                        /*typePropertyPaths=*/ Collections.emptyMap());
 
-        VisibilityDocument expectedDocument1 =
-                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+        GenericDocument expectedDocument1 = VisibilityToDocumentConverter.createVisibilityDocument(
+                new InternalVisibilityConfig.Builder(prefix + "Schema1")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
-                        .build();
-        VisibilityDocument expectedDocument2 =
-                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+                        .build());
+        GenericDocument expectedDocument2 = VisibilityToDocumentConverter.createVisibilityDocument(
+                new InternalVisibilityConfig.Builder(prefix + "Schema2")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
-                        .build();
+                        .build());
+
+        // Ignore the creation timestamp
+        actualDocument1 = new GenericDocument.Builder<>(actualDocument1)
+                .setCreationTimestampMillis(0).build();
+        actualDocument2 = new GenericDocument.Builder<>(actualDocument2)
+                .setCreationTimestampMillis(0).build();
+
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
         assertThat(actualDocument2).isEqualTo(expectedDocument2);
         appSearchImpl.close();
@@ -197,8 +206,8 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImpl.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
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
index a70da64..4b837ae 100644
--- 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
@@ -23,9 +23,9 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -66,17 +66,21 @@
         // Values for a "foo" client
         String packageNameFoo = "packageFoo";
         byte[] sha256CertFoo = new byte[32];
+        PackageIdentifier packageIdentifierFoo =
+                new PackageIdentifier(packageNameFoo, sha256CertFoo);
 
         // Values for a "bar" client
         String packageNameBar = "packageBar";
         byte[] sha256CertBar = new byte[32];
+        PackageIdentifier packageIdentifierBar =
+                new PackageIdentifier(packageNameBar, sha256CertBar);
 
         // Create AppSearchImpl with visibility document version 1;
         AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV1.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
@@ -125,24 +129,24 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
-        VisibilityDocument actualDocument = new VisibilityDocument.Builder(
-                appSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
-                        /*id=*/ prefix + "Schema",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        appSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Schema",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
 
-        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(
+        assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+        assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+                .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
index 056acea..5ac5bda 100644
--- 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
@@ -21,9 +21,13 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
@@ -52,22 +56,21 @@
     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();
+        File appSearchDir = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                mAppSearchDir,
+                appSearchDir,
                 new AppSearchConfigImpl(
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         mVisibilityStore = new VisibilityStore(mAppSearchImpl);
     }
 
@@ -99,55 +102,83 @@
     }
 
     @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);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
+    public void testSetVisibilitySchema() throws Exception {
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+
+
+        GetSchemaResponse getAndroidVOverlaySchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+    }
+
+    @Test
+    public void testSetAndGetVisibility() throws Exception {
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        InternalVisibilityConfig visibilityConfig =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        GenericDocument actualDocument = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                 /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        assertThat(actualDocument).isEqualTo(visibilityDocument);
+                /*typePropertyPaths=*/ Collections.emptyMap());
+        // Ignore the creation timestamp
+        actualDocument =
+                new GenericDocument.Builder<>(actualDocument).setCreationTimestampMillis(0).build();
+
+        assertThat(actualDocument).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
     }
 
     @Test
     public void testRemoveVisibility() throws Exception {
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
-        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
 
         assertThat(mVisibilityStore.getVisibility("Email"))
-                .isEqualTo(visibilityDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument = new VisibilityDocument.Builder(
-                mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        assertThat(actualDocument).isEqualTo(visibilityDocument);
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
+        assertThat(actualConfig).isEqualTo(visibilityConfig);
 
-        mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityDocument.getId()));
+        mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityConfig.getSchemaType()));
         assertThat(mVisibilityStore.getVisibility("Email")).isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the VisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -158,17 +189,17 @@
     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();
+        AppSearchSchema brokenSchema = new AppSearchSchema.Builder(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA_TYPE).build();
 
         // Index a broken schema into AppSearch, use the latest version to make it broken.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                 Collections.singletonList(brokenSchema),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
-                /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
                 /*setSchemaStatsBuilder=*/ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         // Create VisibilityStore should recover the broken schema
@@ -176,13 +207,270 @@
 
         // We should be able to set and get Visibility settings.
         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder(
+                prefix + "Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
-        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
 
         assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
-                .isEqualTo(visibilityDocument);
+                .isEqualTo(visibilityConfig);
+    }
+
+    @Test
+    public void testSetGetAndRemoveOverlayVisibility() throws Exception {
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        SchemaVisibilityConfig nestedvisibilityConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        InternalVisibilityConfig visibilityConfig =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .addVisibleToConfig(nestedvisibilityConfig)
+                        .build();
+
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        GenericDocument visibleToConfigOverlay = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ prefix + "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+        // Ignore the creation timestamp
+        visibleToConfigOverlay = new GenericDocument.Builder<>(visibleToConfigOverlay)
+                .setCreationTimestampMillis(0).build();
+        assertThat(visibleToConfigOverlay).isEqualTo(VisibilityToDocumentConverter
+                .createAndroidVOverlay(visibilityConfig));
+
+        mVisibilityStore.removeVisibility(ImmutableSet.of(prefix + "Email"));
+        // Verify the VisibilityConfig is removed from AppSearchImpl.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ prefix + "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testSetVisibility_avoidRemoveOverlay() throws Exception {
+        // Set a visibility config w/o overlay
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // Put a fake AndroidVOverlay into AppSearchImpl, this is not added by VisibilityStore,
+        // just add a fake AndroidVOverlay to verify we won't remove it when we update the config
+        // which doesn't contain any overlay settings.
+        GenericDocument fakeAndroidVOverlay =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+                        "Email", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .build();
+        mAppSearchImpl.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                fakeAndroidVOverlay,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/null);
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify we won't trigger a remove() call to AppSearchImpl by get the fakeAndroidVOverlay.
+        GenericDocument actualAndroidVOverlay = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // Ignore the creation timestamp
+        actualAndroidVOverlay = new GenericDocument.Builder<>(actualAndroidVOverlay)
+                .setCreationTimestampMillis(0).build();
+        assertThat(actualAndroidVOverlay).isEqualTo(fakeAndroidVOverlay);
+    }
+
+    @Test
+    public void testSetVisibility_removeOverlay_publicAcl() throws Exception {
+        // Set a visibility config with public overlay
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // verify the overlay document is created.
+        mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify the overlay document is removed.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testSetVisibility_removeOverlay_visibleToConfig() throws Exception {
+        // Set a visibility config with visible to config.
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToConfig(new SchemaVisibilityConfig.Builder()
+                        .addRequiredPermissions(ImmutableSet.of(1)).build())
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // verify the overlay document is created.
+        mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify the overlay document is removed.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testMigrateFromDeprecatedSchema() throws Exception {
+        // Set deprecated public acl schema to main visibility database.
+        mAppSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                ImmutableList.of(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA,
+                VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA),
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Create VisibilityStore with success and force remove deprecated public acl schema from
+        // the main visibility database.
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+    }
+
+    @Test
+    public void testMigrateFromDeprecatedOverlaySchema() throws Exception {
+        // Set deprecated overlay schema to overlay database.
+        AppSearchSchema deprecatedOverlaySchema =
+                new AppSearchSchema.Builder("AndroidVOverlayType")
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackage")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackageSha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "visibleToConfigProperty",
+                                "VisibleToConfigType")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .build();
+        AppSearchSchema deprecatedVisibleToConfigSchema =
+                new AppSearchSchema.Builder("VisibleToConfigType")
+                        .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                "notPlatformSurfaceable")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "packageName")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "sha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "permission", VisibilityPermissionConfig.SCHEMA_TYPE)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackage")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackageSha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .build();
+        mAppSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                ImmutableList.of(deprecatedOverlaySchema, deprecatedVisibleToConfigSchema,
+                        VisibilityPermissionConfig.SCHEMA),
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ VisibilityToDocumentConverter
+                        .OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Create VisibilityStore with success and force remove override overlay schema.
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST);
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
new file mode 100644
index 0000000..7c50c0d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.GenericDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.SetSchemaRequest;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class VisibilityToDocumentConverterTest {
+
+    @Test
+    public void testToGenericDocuments() throws Exception {
+        // Create a SetSchemaRequest for testing
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+
+        SchemaVisibilityConfig visibleToConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+                .setSchemaTypeDisplayedBySystem("someSchema", false)
+                .setSchemaTypeVisibilityForPackage("someSchema", true,
+                        new PackageIdentifier("com.example.test3", cert3))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        "someSchema", ImmutableSet.of(3, 4))
+                .setPubliclyVisibleSchema("someSchema",
+                        new PackageIdentifier("com.example.test4", cert4))
+                .addSchemaTypeVisibleToConfig("someSchema", visibleToConfig)
+                .build();
+
+        // Create android V overlay proto
+        VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+                .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test1")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("com.example.test2")
+                                .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+                .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+                        .addAllPermissions(ImmutableSet.of(1, 2)).build())
+                .build();
+        VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test4")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+                .build();
+        AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(visibilityConfigProto)
+                .addVisibleToConfigs(visibleToConfigProto)
+                .build();
+
+        // Create the expected AndroidVOverlay document
+        GenericDocument expectedAndroidVOverlay =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+                        "someSchema", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBytes("visibilityProtoSerializeProperty",
+                                overlayProto.toByteArray())
+                        .build();
+
+        // Create the expected visibility document
+        GenericDocument permissionDoc34 =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("", "",
+                        "VisibilityPermissionType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyLong("allRequiredPermissions", 3, 4).build();
+        GenericDocument expectedVisibilityDocument =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("", "someSchema",
+                        "VisibilityType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBoolean("notPlatformSurfaceable", true)
+                        .setPropertyString("packageName", "com.example.test3")
+                        .setPropertyBytes("sha256Cert", cert3)
+                        .setPropertyDocument("permission", permissionDoc34)
+                        .build();
+
+        // Convert the SetSchemaRequest to a list of VisibilityConfig
+        List<InternalVisibilityConfig> visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+        // Check if the conversion is correct
+        assertThat(visibilityConfigs).hasSize(1);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+        assertThat(expectedVisibilityDocument).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+        assertThat(expectedAndroidVOverlay).isEqualTo(
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig));
+    }
+
+    @Test
+    public void testToVisibilityConfig() throws Exception {
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+
+        // Create visibility proto property
+        VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+                .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test1")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test2")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+                .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+                        .addAllPermissions(ImmutableSet.of(1, 2)).build())
+                .build();
+        VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test4")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+                .build();
+        AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(visibilityConfigProto)
+                .addVisibleToConfigs(visibleToConfigProto)
+                .build();
+
+        // Create a visible config overlay for testing
+        GenericDocument androidVOverlay =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("androidVOverlay",
+                        "someSchema", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBytes("visibilityProtoSerializeProperty",
+                                overlayProto.toByteArray())
+                        .build();
+
+        // Create a VisibilityDocument for testing
+        GenericDocument permissionDoc34 =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("", "",
+                        "VisibilityPermissionType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyLong("allRequiredPermissions", 3, 4).build();
+        GenericDocument visibilityDoc =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("", "someSchema",
+                        "VisibilityType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBoolean("notPlatformSurfaceable", true)
+                        .setPropertyString("packageName", "com.example.test3")
+                        .setPropertyBytes("sha256Cert", cert3)
+                        .setPropertyDocument("permission", permissionDoc34)
+                        .build();
+
+        // Create a VisibilityConfig using the Builder
+        InternalVisibilityConfig visibilityConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        visibilityDoc, androidVOverlay);
+
+        // Check if the properties are set correctly
+        assertThat(visibilityDoc).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+        GenericDocument actualOverlayDoc =
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+        AndroidVOverlayProto actualOverlayProto = AndroidVOverlayProto.parseFrom(
+                actualOverlayDoc.getPropertyBytes("visibilityProtoSerializeProperty"));
+        assertThat(actualOverlayProto).isEqualTo(overlayProto);
+        assertThat(androidVOverlay).isEqualTo(actualOverlayDoc);
+
+        // Verify rebuild from InternalVisibilityConfig remains the same.
+        InternalVisibilityConfig.Builder builder =
+                new InternalVisibilityConfig.Builder(visibilityConfig);
+        InternalVisibilityConfig rebuild = builder.build();
+        assertThat(visibilityConfig).isEqualTo(rebuild);
+
+        InternalVisibilityConfig modifiedConfig = builder
+                .setSchemaType("prefixedSchema")
+                .setNotDisplayedBySystem(false)
+                .addVisibleToPermissions(ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                        SetSchemaRequest.READ_CALENDAR))
+                .clearVisibleToPackages()
+                .addVisibleToPackage(
+                        new PackageIdentifier("com.example.other", new byte[32]))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.other", new byte[32]))
+                .clearVisibleToConfig()
+                .build();
+        assertThat(modifiedConfig.getSchemaType()).isEqualTo("prefixedSchema");
+
+        // Check that the rebuild stayed the same
+        assertThat(rebuild.getSchemaType()).isEqualTo("someSchema");
+        assertThat(rebuild.isNotDisplayedBySystem()).isTrue();
+        assertThat(rebuild.getVisibilityConfig().getRequiredPermissions())
+                .containsExactly(ImmutableSet.of(3, 4));
+        assertThat(rebuild.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(new PackageIdentifier("com.example.test3", cert3));
+        assertThat(
+                rebuild.getVisibilityConfig().getPubliclyVisibleTargetPackage()).isEqualTo(
+                new PackageIdentifier("com.example.test4", cert4));
+
+        SchemaVisibilityConfig expectedVisibleToConfig = new SchemaVisibilityConfig.Builder()
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("com.example.test2", cert2))
+                .build();
+        assertThat(rebuild.getVisibleToConfigs()).containsExactly(expectedVisibleToConfig);
+    }
+
+    @Test
+    public void testToGenericDocumentAndBack() {
+        // Create a SetSchemaRequest for testing
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        byte[] cert5 = new byte[32];
+        byte[] cert6 = new byte[32];
+        byte[] cert7 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+        Arrays.fill(cert5, (byte) 5);
+        Arrays.fill(cert6, (byte) 6);
+        Arrays.fill(cert7, (byte) 7);
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test3", cert3))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+        SchemaVisibilityConfig config3 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test4", cert4))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test5", cert5))
+                .build();
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+                .setSchemaTypeDisplayedBySystem("someSchema", /*displayed=*/ true)
+                .setSchemaTypeVisibilityForPackage("someSchema", /*visible=*/ true,
+                        new PackageIdentifier("com.example.test6", cert6))
+                .addRequiredPermissionsForSchemaTypeVisibility("someSchema",
+                        ImmutableSet.of(1, 2))
+                .setPubliclyVisibleSchema("someSchema",
+                        new PackageIdentifier("com.example.test7", cert7))
+                .addSchemaTypeVisibleToConfig("someSchema", config1)
+                .addSchemaTypeVisibleToConfig("someSchema", config2)
+                .addSchemaTypeVisibleToConfig("someSchema", config3)
+                .build();
+
+        // Convert the SetSchemaRequest to a list of VisibilityConfig
+        List<InternalVisibilityConfig> visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+        GenericDocument visibilityDoc =
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig);
+        GenericDocument androidVOverlay =
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+
+        InternalVisibilityConfig rebuild =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        visibilityDoc, androidVOverlay);
+
+        assertThat(rebuild).isEqualTo(visibilityConfig);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
new file mode 100644
index 0000000..9a98739
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 org.junit.Test;
+
+public class VisibilityUtilTest {
+    @Test
+    public void testIsSchemaSearchableByCaller_selfAccessDefaultAllowed() {
+        CallerAccess callerAccess = new CallerAccess("package1");
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package1",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isTrue();
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package2",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+    }
+
+    @Test
+    public void testIsSchemaSearchableByCaller_selfAccessNotAllowed() {
+        CallerAccess callerAccess = new CallerAccess("package1") {
+            @Override
+            public boolean doesCallerHaveSelfAccess() {
+                return false;
+            }
+        };
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package1",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package2",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+    }
+}
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 c7cccb8..5cecaa7 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
@@ -48,6 +48,12 @@
                 // fall through
             case Features.LIST_FILTER_QUERY_LANGUAGE:
                 // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // fall through
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+                // fall through
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+                // fall through
             case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
                 // fall through
             case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
@@ -60,15 +66,23 @@
                 // fall through
             case Features.SEARCH_SUGGESTION:
                 // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // fall through
             case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
                 // fall through
             case Features.SCHEMA_ADD_PARENT_TYPE:
                 // fall through
+            case Features.SCHEMA_SET_DESCRIPTION:
+                // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
                 // fall through
             case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                // fall through
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
                 return true;
             default:
                 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 4c9fbfe..d175aa6 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,7 +16,6 @@
 
 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.app.InternalSetSchemaResponse.newFailedSetSchemaResponse;
 import static androidx.appsearch.app.InternalSetSchemaResponse.newSuccessfulSetSchemaResponse;
@@ -27,7 +26,6 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
-import android.os.Bundle;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -43,15 +41,16 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SearchSuggestionResult;
 import androidx.appsearch.app.SearchSuggestionSpec;
 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;
@@ -187,12 +186,8 @@
     @VisibleForTesting
     final IcingSearchEngine mIcingSearchEngineLocked;
 
-    // This map contains schema types and SchemaTypeConfigProtos for all package-database
-    // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
-    // prefixed schema type to its respective SchemaTypeConfigProto.
     @GuardedBy("mReadWriteLock")
-    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
-            new ArrayMap<>();
+    private final SchemaCache mSchemaCacheLocked = new SchemaCache();
 
     // This map contains namespaces for all package-database prefixes. All values in the map are
     // prefixed with the package-database prefix.
@@ -259,7 +254,7 @@
      * <p>Instead, logger instance needs to be passed to each individual method, like create, query
      * and putDocument.
      *
-     * @param initStatsBuilder collects stats for initialization if provided.
+     * @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.
@@ -269,8 +264,8 @@
             @NonNull File icingDir,
             @NonNull AppSearchConfig config,
             @Nullable InitializeStats.Builder initStatsBuilder,
-            @NonNull OptimizeStrategy optimizeStrategy,
-            @Nullable VisibilityChecker visibilityChecker)
+            @Nullable VisibilityChecker visibilityChecker,
+            @NonNull OptimizeStrategy optimizeStrategy)
             throws AppSearchException {
         return new AppSearchImpl(icingDir, config, initStatsBuilder, optimizeStrategy,
                 visibilityChecker);
@@ -378,9 +373,12 @@
                 for (int i = 0; i < schemaProtoTypesList.size(); i++) {
                     SchemaTypeConfigProto schema = schemaProtoTypesList.get(i);
                     String prefixedSchemaType = schema.getSchemaType();
-                    addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema);
+                    mSchemaCacheLocked.addToSchemaMap(getPrefix(prefixedSchemaType), schema);
                 }
 
+                // Populate schema parent-to-children map
+                mSchemaCacheLocked.rebuildSchemaParentToChildrenMap();
+
                 // Populate namespace map
                 List<String> prefixedNamespaceList =
                         getAllNamespacesResultProto.getNamespacesList();
@@ -463,10 +461,10 @@
      * @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
+     * @param visibilityConfigs           {@link InternalVisibilityConfig}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}
+     *                                    that don't has a {@link InternalVisibilityConfig}
      *                                    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
@@ -490,7 +488,7 @@
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
-            @NonNull List<VisibilityDocument> visibilityDocuments,
+            @NonNull List<InternalVisibilityConfig> visibilityConfigs,
             boolean forceOverride,
             int version,
             @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -508,7 +506,7 @@
                         packageName,
                         databaseName,
                         schemas,
-                        visibilityDocuments,
+                        visibilityConfigs,
                         forceOverride,
                         version,
                         setSchemaStatsBuilder);
@@ -517,7 +515,7 @@
                         packageName,
                         databaseName,
                         schemas,
-                        visibilityDocuments,
+                        visibilityConfigs,
                         forceOverride,
                         version,
                         setSchemaStatsBuilder);
@@ -539,7 +537,7 @@
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
-            @NonNull List<VisibilityDocument> visibilityDocuments,
+            @NonNull List<InternalVisibilityConfig> visibilityConfigs,
             boolean forceOverride,
             int version,
             @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -588,7 +586,7 @@
                 packageName,
                 databaseName,
                 schemas,
-                visibilityDocuments,
+                visibilityConfigs,
                 forceOverride,
                 version,
                 setSchemaStatsBuilder);
@@ -717,7 +715,7 @@
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
-            @NonNull List<VisibilityDocument> visibilityDocuments,
+            @NonNull List<InternalVisibilityConfig> visibilityConfigs,
             boolean forceOverride,
             int version,
             @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException {
@@ -791,42 +789,47 @@
         // Update derived data structures.
         for (SchemaTypeConfigProto schemaTypeConfigProto :
                 rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
-            addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+            mSchemaCacheLocked.addToSchemaMap(prefix, schemaTypeConfigProto);
         }
 
         for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
-            removeFromMap(mSchemaMapLocked, prefix, schemaType);
+            mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType);
         }
+
+        mSchemaCacheLocked.rebuildSchemaParentToChildrenMapForPrefix(prefix);
+
         // 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
+            List<InternalVisibilityConfig> prefixedVisibilityConfigs =
+                    new ArrayList<>(visibilityConfigs.size());
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+                // The VisibilityConfig 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.Builder(
-                                unPrefixedDocument).setId(prefixedSchemaType).build());
+                // TODO(b/275592563): Move prefixing into VisibilityConfig.createVisibilityDocument
+                //  and createVisibilityOverlay
+                String schemaType = visibilityConfig.getSchemaType();
+                String prefixedSchemaType = prefix + schemaType;
+                prefixedVisibilityConfigs.add(new InternalVisibilityConfig.Builder(visibilityConfig)
+                        .setSchemaType(prefixedSchemaType).build());
                 // This schema has visibility settings. We should keep it from the removal list.
-                deprecatedVisibilityDocuments.remove(prefixedSchemaType);
+                deprecatedVisibilityDocuments.remove(visibilityConfig.getSchemaType());
             }
             // 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);
+            mVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs);
         }
         long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime();
         if (setSchemaStatsBuilder != null) {
@@ -904,33 +907,44 @@
                 // schema. Avoid call visibility store before we have already created it.
                 if (mVisibilityStoreLocked != null) {
                     String typeName = typeConfig.getSchemaType().substring(typePrefix.length());
-                    VisibilityDocument visibilityDocument =
+                    InternalVisibilityConfig visibilityConfig =
                             mVisibilityStoreLocked.getVisibility(prefixedSchemaType);
-                    if (visibilityDocument != null) {
-                        if (visibilityDocument.isNotDisplayedBySystem()) {
-                            responseBuilder
-                                    .addSchemaTypeNotDisplayedBySystem(typeName);
+                    if (visibilityConfig != null) {
+                        if (visibilityConfig.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]));
-                            }
+                        List<PackageIdentifier> packageIdentifiers =
+                                visibilityConfig.getVisibilityConfig().getAllowedPackages();
+                        if (!packageIdentifiers.isEmpty()) {
                             responseBuilder.setSchemaTypeVisibleToPackages(typeName,
-                                    packageIdentifier);
+                                    new ArraySet<>(packageIdentifiers));
                         }
                         Set<Set<Integer>> visibleToPermissions =
-                                visibilityDocument.getVisibleToPermissions();
-                        if (visibleToPermissions != null) {
-                            responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(
-                                    typeName,  visibleToPermissions);
+                                visibilityConfig.getVisibilityConfig().getRequiredPermissions();
+                        if (!visibleToPermissions.isEmpty()) {
+                            Set<Set<Integer>> visibleToPermissionsSet =
+                                    new ArraySet<>(visibleToPermissions.size());
+                            for (Set<Integer> permissionList : visibleToPermissions) {
+                                visibleToPermissionsSet.add(new ArraySet<>(permissionList));
+                            }
+
+                            responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(typeName,
+                                    visibleToPermissionsSet);
+                        }
+
+                        // Check for Visibility properties from the overlay
+                        PackageIdentifier publiclyVisibleFromPackage =
+                                visibilityConfig.getVisibilityConfig()
+                                        .getPubliclyVisibleTargetPackage();
+                        if (publiclyVisibleFromPackage != null) {
+                            responseBuilder.setPubliclyVisibleSchema(
+                                    typeName, publiclyVisibleFromPackage);
+                        }
+                        Set<SchemaVisibilityConfig> visibleToConfigs =
+                                visibilityConfig.getVisibleToConfigs();
+                        if (!visibleToConfigs.isEmpty()) {
+                            responseBuilder.setSchemaTypeVisibleToConfigs(
+                                    typeName, visibleToConfigs);
                         }
                     }
                 }
@@ -1190,7 +1204,7 @@
             removePrefixesFromDocument(documentBuilder);
             String prefix = createPrefix(packageName, databaseName);
             Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                    Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
+                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
                     prefix, schemaTypeMap, mConfig);
         } finally {
@@ -1232,7 +1246,7 @@
             // 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 =
-                    Preconditions.checkNotNull(mSchemaMapLocked.get(prefix));
+                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
                     prefix, schemaTypeMap, mConfig);
         } finally {
@@ -1254,6 +1268,8 @@
      */
     @NonNull
     @GuardedBy("mReadWriteLock")
+    // We only log getResultProto.toString() in fullPii trace for debugging.
+    @SuppressWarnings("LiteProtoToString")
     private DocumentProto getDocumentProtoByIdLocked(
             @NonNull String packageName,
             @NonNull String databaseName,
@@ -1317,8 +1333,9 @@
         SearchStats.Builder sStatsBuilder = null;
         if (logger != null) {
             sStatsBuilder =
-                    new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL,
-                            packageName).setDatabase(databaseName);
+                    new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName)
+                            .setDatabase(databaseName)
+                            .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag());
         }
 
         long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();
@@ -1338,18 +1355,18 @@
                 if (sStatsBuilder != null && logger != null) {
                     sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR);
                 }
-                return new SearchResultPage(Bundle.EMPTY);
+                return new SearchResultPage();
             }
 
             String prefix = createPrefix(packageName, databaseName);
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec,
-                            Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
+                            Collections.singleton(prefix), mNamespaceMapLocked, mSchemaCacheLocked,
                             mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
                 // empty SearchResult and skip sending request to Icing.
-                return new SearchResultPage(Bundle.EMPTY);
+                return new SearchResultPage();
             }
 
             SearchResultPage searchResultPage =
@@ -1394,7 +1411,8 @@
             sStatsBuilder =
                     new SearchStats.Builder(
                             SearchStats.VISIBILITY_SCOPE_GLOBAL,
-                            callerAccess.getCallingPackageName());
+                            callerAccess.getCallingPackageName())
+                            .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag());
         }
 
         long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime();
@@ -1417,13 +1435,13 @@
             // SearchSpec that wants to query every visible package.
             Set<String> packageFilters = new ArraySet<>();
             if (!searchSpec.getFilterPackageNames().isEmpty()) {
-                if (searchSpec.getJoinSpec() == null) {
+                JoinSpec joinSpec = searchSpec.getJoinSpec();
+                if (joinSpec == null) {
                     packageFilters.addAll(searchSpec.getFilterPackageNames());
-                } else if (!searchSpec.getJoinSpec().getNestedSearchSpec()
+                } else if (!joinSpec.getNestedSearchSpec()
                         .getFilterPackageNames().isEmpty()) {
                     packageFilters.addAll(searchSpec.getFilterPackageNames());
-                    packageFilters.addAll(
-                            searchSpec.getJoinSpec().getNestedSearchSpec().getFilterPackageNames());
+                    packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames());
                 }
             }
 
@@ -1445,14 +1463,14 @@
             }
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec, prefixFilters,
-                            mNamespaceMapLocked, mSchemaMapLocked, mConfig);
+                            mNamespaceMapLocked, mSchemaCacheLocked, mConfig);
             // Remove those inaccessible schemas.
             searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
                     callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
                 // empty SearchResult and skip sending request to Icing.
-                return new SearchResultPage(Bundle.EMPTY);
+                return new SearchResultPage();
             }
             if (sStatsBuilder != null) {
                 sStatsBuilder.setAclCheckLatencyMillis(
@@ -1486,7 +1504,7 @@
         long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
         SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto();
         ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto(
-                mNamespaceMapLocked, mSchemaMapLocked);
+                mNamespaceMapLocked, mSchemaCacheLocked);
         ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
         if (sStatsBuilder != null) {
             sStatsBuilder.setRewriteSearchSpecLatencyMillis((int)
@@ -1500,7 +1518,7 @@
         long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
         // Rewrite search result before we return.
         SearchResultPage searchResultPage = SearchResultToProtoConverter
-                .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
+                .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig);
         if (sStatsBuilder != null) {
             sStatsBuilder.setRewriteSearchResultLatencyMillis(
                     (int) (SystemClock.elapsedRealtime()
@@ -1510,6 +1528,8 @@
     }
 
     @GuardedBy("mReadWriteLock")
+    // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging.
+    @SuppressWarnings("LiteProtoToString")
     private SearchResultProto searchInIcingLocked(
             @NonNull SearchSpecProto searchSpec,
             @NonNull ResultSpecProto resultSpec,
@@ -1581,7 +1601,7 @@
                             searchSuggestionSpec,
                             Collections.singleton(prefix),
                             mNamespaceMapLocked,
-                            mSchemaMapLocked);
+                            mSchemaCacheLocked);
 
             if (searchSuggestionSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
@@ -1616,7 +1636,7 @@
         mReadWriteLock.readLock().lock();
         try {
             Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
-            for (String prefix : mSchemaMapLocked.keySet()) {
+            for (String prefix : mSchemaCacheLocked.getAllPrefixes()) {
                 String packageName = getPackageName(prefix);
 
                 Set<String> databases = packageToDatabases.get(packageName);
@@ -1696,7 +1716,7 @@
             long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
             // Rewrite search result before we return.
             SearchResultPage searchResultPage = SearchResultToProtoConverter
-                    .toSearchResultPage(searchResultProto, mSchemaMapLocked, mConfig);
+                    .toSearchResultPage(searchResultProto, mSchemaCacheLocked, mConfig);
             if (sStatsBuilder != null) {
                 sStatsBuilder.setRewriteSearchResultLatencyMillis(
                         (int) (SystemClock.elapsedRealtime()
@@ -1910,7 +1930,7 @@
 
             SearchSpecToProtoConverter searchSpecToProtoConverter =
                     new SearchSpecToProtoConverter(queryExpression, searchSpec,
-                            Collections.singleton(prefix), mNamespaceMapLocked, mSchemaMapLocked,
+                            Collections.singleton(prefix), mNamespaceMapLocked, mSchemaCacheLocked,
                             mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return
@@ -2337,10 +2357,9 @@
                     }
                     for (String databaseName : databaseNames) {
                         String removedPrefix = createPrefix(packageName, databaseName);
-                        Map<String, SchemaTypeConfigProto> removedSchemas =
-                                Preconditions.checkNotNull(mSchemaMapLocked.remove(removedPrefix));
+                        Set<String> removedSchemas = mSchemaCacheLocked.removePrefix(removedPrefix);
                         if (mVisibilityStoreLocked != null) {
-                            mVisibilityStoreLocked.removeVisibility(removedSchemas.keySet());
+                            mVisibilityStoreLocked.removeVisibility(removedSchemas);
                         }
 
                         mNamespaceMapLocked.remove(removedPrefix);
@@ -2370,7 +2389,7 @@
                 resetResultProto.getStatus(),
                 resetResultProto);
         mOptimizeIntervalCountLocked = 0;
-        mSchemaMapLocked.clear();
+        mSchemaCacheLocked.clear();
         mNamespaceMapLocked.clear();
         mDocumentCountMapLocked.clear();
         synchronized (mNextPageTokensLocked) {
@@ -2616,24 +2635,6 @@
         values.add(prefixedValue);
     }
 
-    private static void addToMap(Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix,
-            SchemaTypeConfigProto schemaTypeConfigProto) {
-        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
-        if (schemaTypeMap == null) {
-            schemaTypeMap = new ArrayMap<>();
-            map.put(prefix, schemaTypeMap);
-        }
-        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
-    }
-
-    private static void removeFromMap(Map<String, Map<String, SchemaTypeConfigProto>> map,
-            String prefix, String schemaType) {
-        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
-        if (schemaTypeMap != null) {
-            schemaTypeMap.remove(schemaType);
-        }
-    }
-
     /**
      * Checks the given status code and throws an {@link AppSearchException} if code is an error.
      *
@@ -2758,22 +2759,26 @@
         }
         if (LogUtil.DEBUG) {
             if (Log.isLoggable(icingTag, Log.VERBOSE)) {
-                IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE, /*verbosity=*/
-                        (short) 1);
+                boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE,
+                        /*verbosity=*/ (short) 1);
                 return;
             } else if (Log.isLoggable(icingTag, Log.DEBUG)) {
-                IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
+                boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
                 return;
             }
         }
-        if (Log.isLoggable(icingTag, Log.INFO)) {
-            IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
-        } else if (Log.isLoggable(icingTag, Log.WARN)) {
-            IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);
+        if (LogUtil.INFO) {
+            if (Log.isLoggable(icingTag, Log.INFO)) {
+                boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
+                return;
+            }
+        }
+        if (Log.isLoggable(icingTag, Log.WARN)) {
+            boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);
         } else if (Log.isLoggable(icingTag, Log.ERROR)) {
-            IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);
+            boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);
         } else {
-            IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL);
+            boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL);
         }
     }
 
@@ -2795,11 +2800,7 @@
     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;
+            return mSchemaCacheLocked.getAllPrefixedSchemaTypes();
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -2814,7 +2815,7 @@
      *                    {@link AppSearchResult} code.
      * @return {@link AppSearchResult} error code
      */
-    private static @AppSearchResult.ResultCode int statusProtoToResultCode(
+    @AppSearchResult.ResultCode private static int statusProtoToResultCode(
             @NonNull StatusProto statusProto) {
         return ResultCodeToProtoConverter.toResultCode(statusProto.getCode());
     }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
index 3e9e739..4e54764 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
@@ -23,15 +23,18 @@
 import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.stats.PutDocumentStats;
 import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
 import androidx.appsearch.localstorage.stats.SearchStats;
 import androidx.appsearch.localstorage.stats.SetSchemaStats;
 import androidx.appsearch.stats.SchemaMigrationStats;
 
+import java.util.List;
+
 /**
  * An interface for implementing client-defined logging AppSearch operations stats.
  *
  * <p>Any implementation needs to provide general information on how to log all the stats types.
- * (e.g. {@link CallStats})
+ * (for example {@link CallStats})
  *
  * <p>All implementations of this interface must be thread safe.
  *
@@ -42,42 +45,79 @@
     /**
      * Logs {@link CallStats}
      */
-    void logStats(@NonNull CallStats stats);
+    default void logStats(@NonNull CallStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link PutDocumentStats}
      */
-    void logStats(@NonNull PutDocumentStats stats);
+    default void logStats(@NonNull PutDocumentStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link InitializeStats}
      */
-    void logStats(@NonNull InitializeStats stats);
+    default void logStats(@NonNull InitializeStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link SearchStats}
      */
-    void logStats(@NonNull SearchStats stats);
+    default void logStats(@NonNull SearchStats stats) {
+        // no-op
+    }
 
-    /**
-     * Logs {@link RemoveStats}
-     */
-    void logStats(@NonNull RemoveStats stats);
+    /** Logs {@link RemoveStats} */
+    default void logStats(@NonNull RemoveStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link OptimizeStats}
      */
-    void logStats(@NonNull OptimizeStats stats);
+    default void logStats(@NonNull OptimizeStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link SetSchemaStats}
      */
-    void logStats(@NonNull SetSchemaStats stats);
+    default void logStats(@NonNull SetSchemaStats stats) {
+        // no-op
+    }
 
     /**
      * Logs {@link SchemaMigrationStats}
      */
-    void logStats(@NonNull SchemaMigrationStats stats);
+    default void logStats(@NonNull SchemaMigrationStats stats) {
+        // no-op
+    }
+
+    /**
+     * Logs a list of {@link SearchSessionStats}.
+     *
+     * <p>Since the client app may report search intents belonging to different search sessions in a
+     * single taken action reporting request, the stats extractor will separate them into multiple
+     * search sessions. Therefore, we need a list of {@link SearchSessionStats} here.
+     *
+     * <p>For example, the client app reports the following search intent sequence:
+     *
+     * <ul>
+     *   <li>t = 1, the user searches "a" with some clicks.
+     *   <li>t = 5, the user searches "app" with some clicks.
+     *   <li>t = 10000, the user searches "email" with some clicks.
+     * </ul>
+     *
+     * The extractor will detect "email" belongs to a completely independent search session, and
+     * creates 2 {@link SearchSessionStats} with search intents ["a", "app"] and ["email"]
+     * respectively.
+     */
+    default void logStats(@NonNull List<SearchSessionStats> searchSessionsStats) {
+        // no-op
+    }
 
     // TODO(b/173532925) Add remaining logStats once we add all the stats.
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
index 652fc94..74ed1c2 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -169,11 +169,9 @@
         Preconditions.checkNotNull(fromNativeStats);
         Preconditions.checkNotNull(toStatsBuilder);
 
-        @SuppressWarnings("deprecation")
-        int deleteType = DeleteStatsProto.DeleteType.Code.DEPRECATED_QUERY.getNumber();
         toStatsBuilder
                 .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
-                .setDeleteType(deleteType)
+                .setDeleteType(RemoveStats.QUERY)
                 .setDeletedDocumentCount(fromNativeStats.getNumDocumentsDeleted());
     }
 
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 659433a..e040e50 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
@@ -19,7 +19,6 @@
 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
 
-import android.os.Bundle;
 import android.os.Parcel;
 
 import androidx.annotation.NonNull;
@@ -32,6 +31,7 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
 import androidx.appsearch.stats.SchemaMigrationStats;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -130,11 +130,11 @@
                                         + newDocument.getSchemaType()
                                         + ". But the schema types doesn't exist in the request");
                     }
-                    Bundle bundle = newDocument.getBundle();
+                    GenericDocumentParcel documentParcel = newDocument.getDocumentParcel();
                     byte[] serializedMessage;
                     Parcel parcel = Parcel.obtain();
                     try {
-                        parcel.writeBundle(bundle);
+                        documentParcel.writeToParcel(parcel, /* flags= */ 0);
                         serializedMessage = parcel.marshall();
                     } finally {
                         parcel.recycle();
@@ -224,17 +224,17 @@
             @NonNull CodedInputStream codedInputStream) throws IOException {
         byte[] serializedMessage = codedInputStream.readByteArray();
 
-        Bundle bundle;
+        GenericDocumentParcel documentParcel;
         Parcel parcel = Parcel.obtain();
         try {
             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
             parcel.setDataPosition(0);
-            bundle = parcel.readBundle();
+            documentParcel = GenericDocumentParcel.CREATOR.createFromParcel(parcel);
         } finally {
             parcel.recycle();
         }
 
-        return new GenericDocument(bundle);
+        return new GenericDocument(documentParcel);
     }
 
     @Override
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 ef8ec3b..7597399 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
@@ -93,7 +93,7 @@
             AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+            Map<String, List<String>> typePropertyPaths = request.getProjections();
             CallerAccess access = new CallerAccess(mContext.getPackageName());
             for (String id : request.getIds()) {
                 try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
index 870df5a..d3246bf 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -36,7 +36,7 @@
 
     boolean DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT = false;
 
-    float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.0f;
+    float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.9f;
 
     /**
      * The default compression level in IcingSearchEngineOptions proto matches the
@@ -62,7 +62,7 @@
      */
     int DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD = 65536;
 
-    boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = false;
+    boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = true;
 
     /**
      * The default sort threshold for the lite index when sort at indexing is enabled.
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 db55481..bc9715b 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
@@ -26,6 +26,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -39,7 +40,6 @@
 import java.io.File;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * An AppSearch storage system which stores data locally in the app's storage space using a bundled
@@ -56,8 +56,6 @@
 public class LocalStorage {
     private static final String TAG = "AppSearchLocalStorage";
 
-    private static final String ICING_LIB_ROOT_DIR = "appsearch";
-
     /** Contains information about how to create the search session. */
     public static final class SearchContext {
         final Context mContext;
@@ -249,7 +247,8 @@
 
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     private static volatile LocalStorage sInstance;
 
@@ -326,7 +325,8 @@
             @Nullable AppSearchLogger logger)
             throws AppSearchException {
         Preconditions.checkNotNull(context);
-        File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
+        File icingDir = AppSearchEnvironmentFactory.getEnvironmentInstance()
+                .getAppSearchDir(context, /* userHandle= */ null);
 
         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
         InitializeStats.Builder initStatsBuilder = null;
@@ -346,8 +346,8 @@
                         /* shouldRetrieveParentInfo= */ true
                 ),
                 initStatsBuilder,
-                new JetpackOptimizeStrategy(),
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                new JetpackOptimizeStrategy());
 
         if (logger != null) {
             initStatsBuilder.setTotalLatencyMillis(
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
index 74ea1c7..11b530e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
@@ -96,6 +96,6 @@
 
     @Override
     public boolean getBuildPropertyExistenceMetadataHits() {
-        return DEFAULT_BUILD_PROPERTY_EXISTENCE_METADATA_HITS;
+        return true;
     }
 }
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 d7c46ef..907a078 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
@@ -31,6 +31,7 @@
 import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.appsearch.util.ExceptionUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
@@ -75,8 +76,12 @@
 
         @Override
         public boolean equals(@Nullable Object o) {
-            if (this == o) return true;
-            if (!(o instanceof DocumentChangeGroupKey)) return false;
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof DocumentChangeGroupKey)) {
+                return false;
+            }
             DocumentChangeGroupKey that = (DocumentChangeGroupKey) o;
             return mPackageName.equals(that.mPackageName)
                     && mDatabaseName.equals(that.mDatabaseName)
@@ -410,8 +415,9 @@
 
                     try {
                         observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
-                    } catch (Throwable t) {
-                        Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                    } catch (RuntimeException e) {
+                        Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+                        ExceptionUtil.handleException(e);
                     }
                 }
             }
@@ -429,8 +435,9 @@
 
                     try {
                         observerInfo.mObserverCallback.onDocumentChanged(documentChangeInfo);
-                    } catch (Throwable t) {
-                        Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                    } catch (RuntimeException e) {
+                        Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+                        ExceptionUtil.handleException(e);
                     }
                 }
             }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
new file mode 100644
index 0000000..7138182
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SchemaCache.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Caches and manages schema information for AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaCache {
+    /**
+     * A map that contains schema types and SchemaTypeConfigProtos for all package-database
+     * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+     * prefixed schema type to its respective SchemaTypeConfigProto.
+     */
+    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap = new ArrayMap<>();
+
+    /**
+     * A map that contains schema types and all children schema types for all package-database
+     * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+     * prefixed schema type to its respective list of children prefixed schema types.
+     */
+    private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap =
+            new ArrayMap<>();
+
+    public SchemaCache() {
+    }
+
+    public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        mSchemaMap.putAll(Preconditions.checkNotNull(schemaMap));
+        rebuildSchemaParentToChildrenMap();
+    }
+
+    /**
+     * Returns the schema map for the given prefix.
+     */
+    @NonNull
+    public Map<String, SchemaTypeConfigProto> getSchemaMapForPrefix(@NonNull String prefix) {
+        Preconditions.checkNotNull(prefix);
+
+        Map<String, SchemaTypeConfigProto> schemaMap = mSchemaMap.get(prefix);
+        if (schemaMap == null) {
+            return Collections.emptyMap();
+        }
+        return schemaMap;
+    }
+
+    /**
+     * Returns a set of all prefixes stored in the cache.
+     */
+    @NonNull
+    public Set<String> getAllPrefixes() {
+        return Collections.unmodifiableSet(mSchemaMap.keySet());
+    }
+
+    /**
+     * Returns all prefixed schema types stored in the cache.
+     *
+     * <p>This method is inefficient to call repeatedly.
+     */
+    @NonNull
+    public List<String> getAllPrefixedSchemaTypes() {
+        List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
+        for (Map<String, SchemaTypeConfigProto> value : mSchemaMap.values()) {
+            cachedPrefixedSchemaTypes.addAll(value.keySet());
+        }
+        return cachedPrefixedSchemaTypes;
+    }
+
+    /**
+     * Returns the schema types for the given set of prefixed schema types with their
+     * descendants, based on the schema parent-to-children map held in the cache.
+     */
+    @NonNull
+    public Set<String> getSchemaTypesWithDescendants(@NonNull String prefix,
+            @NonNull Set<String> prefixedSchemaTypes) {
+        Preconditions.checkNotNull(prefix);
+        Preconditions.checkNotNull(prefixedSchemaTypes);
+        Map<String, List<String>> parentToChildrenMap = mSchemaParentToChildrenMap.get(prefix);
+        if (parentToChildrenMap == null) {
+            parentToChildrenMap = Collections.emptyMap();
+        }
+
+        // Perform a BFS search on the inheritance graph started by the set of prefixedSchemaTypes.
+        Set<String> visited = new ArraySet<>();
+        Queue<String> prefixedSchemaQueue = new ArrayDeque<>(prefixedSchemaTypes);
+        while (!prefixedSchemaQueue.isEmpty()) {
+            String currentPrefixedSchema = prefixedSchemaQueue.poll();
+            if (visited.contains(currentPrefixedSchema)) {
+                continue;
+            }
+            visited.add(currentPrefixedSchema);
+            List<String> children = parentToChildrenMap.get(currentPrefixedSchema);
+            if (children == null) {
+                continue;
+            }
+            prefixedSchemaQueue.addAll(children);
+        }
+
+        return visited;
+    }
+
+    /**
+     * Rebuilds the schema parent-to-children map for the given prefix, based on the current
+     * schema map.
+     *
+     * <p>The schema parent-to-children map is required to be updated when
+     * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
+     * results from {@link #getSchemaTypesWithDescendants} would be stale.
+     */
+    public void rebuildSchemaParentToChildrenMapForPrefix(@NonNull String prefix) {
+        Preconditions.checkNotNull(prefix);
+
+        mSchemaParentToChildrenMap.remove(prefix);
+        Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix);
+        if (prefixedSchemaMap == null) {
+            return;
+        }
+
+        // Build the parent-to-children map for the current prefix.
+        Map<String, List<String>> parentToChildrenMap = new ArrayMap<>();
+        for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) {
+            for (int i = 0; i < childSchemaConfig.getParentTypesCount(); i++) {
+                String parent = childSchemaConfig.getParentTypes(i);
+                List<String> children = parentToChildrenMap.get(parent);
+                if (children == null) {
+                    children = new ArrayList<>();
+                    parentToChildrenMap.put(parent, children);
+                }
+                children.add(childSchemaConfig.getSchemaType());
+            }
+        }
+
+        // Record the map for the current prefix.
+        if (!parentToChildrenMap.isEmpty()) {
+            mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap);
+        }
+    }
+
+    /**
+     * Rebuilds the schema parent-to-children map based on the current schema map.
+     *
+     * <p>The schema parent-to-children map is required to be updated when
+     * {@link #addToSchemaMap} or {@link #removeFromSchemaMap} has been called. Otherwise, the
+     * results from {@link #getSchemaTypesWithDescendants} would be stale.
+     */
+    public void rebuildSchemaParentToChildrenMap() {
+        mSchemaParentToChildrenMap.clear();
+        for (String prefix : mSchemaMap.keySet()) {
+            rebuildSchemaParentToChildrenMapForPrefix(prefix);
+        }
+    }
+
+    /**
+     * Adds a schema to the schema map.
+     *
+     * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
+     * and either {@link #rebuildSchemaParentToChildrenMap} or
+     * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
+     * cache.
+     */
+    public void addToSchemaMap(@NonNull String prefix,
+            @NonNull SchemaTypeConfigProto schemaTypeConfigProto) {
+        Preconditions.checkNotNull(prefix);
+        Preconditions.checkNotNull(schemaTypeConfigProto);
+
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+        if (schemaTypeMap == null) {
+            schemaTypeMap = new ArrayMap<>();
+            mSchemaMap.put(prefix, schemaTypeMap);
+        }
+        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
+    }
+
+    /**
+     * Removes a schema from the schema map.
+     *
+     * <p>Note that this method will invalidate the schema parent-to-children map in the cache,
+     * and either {@link #rebuildSchemaParentToChildrenMap} or
+     * {@link #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the
+     * cache.
+     */
+    public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) {
+        Preconditions.checkNotNull(prefix);
+        Preconditions.checkNotNull(schemaType);
+
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+        if (schemaTypeMap != null) {
+            schemaTypeMap.remove(schemaType);
+        }
+    }
+
+    /**
+     * Removes the entry of the given prefix from both the schema map and the schema
+     * parent-to-children map, and returns the set of removed prefixed schema type.
+     */
+    @NonNull
+    public Set<String> removePrefix(@NonNull String prefix) {
+        Preconditions.checkNotNull(prefix);
+
+        Map<String, SchemaTypeConfigProto> removedSchemas =
+                Preconditions.checkNotNull(mSchemaMap.remove(prefix));
+        mSchemaParentToChildrenMap.remove(prefix);
+        return removedSchemas.keySet();
+    }
+
+    /**
+     * Clears all data in the cache.
+     */
+    public void clear() {
+        mSchemaMap.clear();
+        mSchemaParentToChildrenMap.clear();
+    }
+}
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 36644b4..8094be0 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
@@ -34,6 +34,7 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
@@ -45,7 +46,6 @@
 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;
@@ -80,7 +80,6 @@
     private final AppSearchImpl mAppSearchImpl;
     private final Executor mExecutor;
     private final Features mFeatures;
-    private final Context mContext;
     private final String mDatabaseName;
     @Nullable private final AppSearchLogger mLogger;
 
@@ -100,11 +99,11 @@
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
         mFeatures = Preconditions.checkNotNull(features);
-        mContext = Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(context);
         mDatabaseName = Preconditions.checkNotNull(databaseName);
         mLogger = logger;
 
-        mPackageName = mContext.getPackageName();
+        mPackageName = context.getPackageName();
         mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
     }
 
@@ -126,15 +125,15 @@
                         mPackageName, mDatabaseName);
             }
 
-            // Extract a Map<schema, VisibilityDocument> from the request.
-            List<VisibilityDocument> visibilityDocuments = VisibilityDocument
-                    .toVisibilityDocuments(request);
+            List<InternalVisibilityConfig> visibilityConfigs =
+                    InternalVisibilityConfig.toInternalVisibilityConfigs(request);
 
             Map<String, Migrator> migrators = request.getMigrators();
             // No need to trigger migration if user never set migrator.
-            if (migrators.size() == 0) {
+            if (migrators.isEmpty()) {
                 SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
-                        visibilityDocuments, firstSetSchemaStatsBuilder);
+                        visibilityConfigs,
+                        firstSetSchemaStatsBuilder);
 
                 long dispatchNotificationStartTimeMillis = SystemClock.elapsedRealtime();
                 // Schedule a task to dispatch change notifications. See requirements for where the
@@ -172,9 +171,9 @@
             Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
                     getSchemaResponse.getSchemas(), migrators, currentVersion, finalVersion);
             // No need to trigger migration if no migrator is active.
-            if (activeMigrators.size() == 0) {
+            if (activeMigrators.isEmpty()) {
                 SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
-                        visibilityDocuments, firstSetSchemaStatsBuilder);
+                        visibilityConfigs, firstSetSchemaStatsBuilder);
                 if (firstSetSchemaStatsBuilder != null) {
                     firstSetSchemaStatsBuilder.setTotalLatencyMillis(
                             (int) (SystemClock.elapsedRealtime() - startMillis));
@@ -194,7 +193,7 @@
                     mPackageName,
                     mDatabaseName,
                     new ArrayList<>(request.getSchemas()),
-                    visibilityDocuments,
+                    visibilityConfigs,
                     /*forceOverride=*/false,
                     request.getVersion(),
                     firstSetSchemaStatsBuilder);
@@ -233,7 +232,7 @@
                             mPackageName,
                             mDatabaseName,
                             new ArrayList<>(request.getSchemas()),
-                            visibilityDocuments,
+                            visibilityConfigs,
                             /*forceOverride=*/ true,
                             request.getVersion(),
                             secondSetSchemaStatsBuilder);
@@ -246,9 +245,8 @@
                     }
                 }
                 long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
-                SetSchemaResponse.Builder responseBuilder = internalSetSchemaResponse
-                        .getSetSchemaResponse()
-                        .toBuilder()
+                SetSchemaResponse.Builder responseBuilder = new SetSchemaResponse.Builder(
+                        internalSetSchemaResponse.getSetSchemaResponse())
                         .addMigratedTypes(activeMigrators.keySet());
                 mIsMutated = true;
 
@@ -352,23 +350,26 @@
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+
+        List<GenericDocument> documents = request.getGenericDocuments();
+        List<GenericDocument> takenActions = request.getTakenActionGenericDocuments();
+
         ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
             AppSearchBatchResult.Builder<String, Void> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
-            for (int i = 0; i < request.getGenericDocuments().size(); i++) {
-                GenericDocument document = request.getGenericDocuments().get(i);
-                try {
-                    mAppSearchImpl.putDocument(
-                            mPackageName,
-                            mDatabaseName,
-                            document,
-                            /*sendChangeNotifications=*/ true,
-                            mLogger);
-                    resultBuilder.setSuccess(document.getId(), /*value=*/ null);
-                } catch (Throwable t) {
-                    resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
-                }
+
+            // Normal documents.
+            for (int i = 0; i < documents.size(); i++) {
+                GenericDocument document = documents.get(i);
+                putGenericDocument(document, resultBuilder);
             }
+
+            // TakenAction documents.
+            for (int i = 0; i < takenActions.size(); i++) {
+                GenericDocument takenActionGenericDocument = takenActions.get(i);
+                putGenericDocument(takenActionGenericDocument, resultBuilder);
+            }
+
             // Now that the batch has been written. Persist the newly written data.
             mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
@@ -382,7 +383,7 @@
 
         // The existing documents with same ID will be deleted, so there may be some resources that
         // could be released after optimize().
-        checkForOptimize(/*mutateBatchSize=*/ request.getGenericDocuments().size());
+        checkForOptimize(/*mutateBatchSize=*/ documents.size() + takenActions.size());
         return future;
     }
 
@@ -396,7 +397,7 @@
             AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+            Map<String, List<String>> typePropertyPaths = request.getProjections();
             for (String id : request.getIds()) {
                 try {
                     GenericDocument document =
@@ -583,7 +584,7 @@
      * forceoverride in the request.
      */
     private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
-            @NonNull List<VisibilityDocument> visibilityDocuments,
+            @NonNull List<InternalVisibilityConfig> visibilityConfigs,
             @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)
             throws AppSearchException {
         if (setSchemaStatsBuilder != null) {
@@ -593,7 +594,7 @@
                 mPackageName,
                 mDatabaseName,
                 new ArrayList<>(request.getSchemas()),
-                visibilityDocuments,
+                visibilityConfigs,
                 request.isForceOverride(),
                 request.getVersion(),
                 setSchemaStatsBuilder);
@@ -621,6 +622,28 @@
         mAppSearchImpl.dispatchAndClearChangeNotifications();
     }
 
+    /**
+     * Calls {@link AppSearchImpl} to put a generic document and sets the result.
+     *
+     * @param document the {@link GenericDocument} to put.
+     * @param resultBuilder an {@link AppSearchBatchResult.Builder} object for collecting the
+     *                      result.
+     */
+    private void putGenericDocument(
+            GenericDocument document, AppSearchBatchResult.Builder<String, Void> resultBuilder) {
+        try {
+            mAppSearchImpl.putDocument(
+                    mPackageName,
+                    mDatabaseName,
+                    document,
+                    /*sendChangeNotifications=*/ true,
+                    mLogger);
+            resultBuilder.setSuccess(document.getId(), /*value=*/ null);
+        } catch (Throwable t) {
+            resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
+        }
+    }
+
     private void checkForOptimize(int mutateBatchSize) {
         mExecutor.execute(() -> {
             long totalLatencyStartMillis = SystemClock.elapsedRealtime();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index aedd191..2320d8b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfig;
@@ -54,6 +55,8 @@
     private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
     private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
     private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
+    private static final EmbeddingVector[] EMPTY_EMBEDDING_ARRAY =
+            new EmbeddingVector[0];
 
     private GenericDocumentToProtoConverter() {
     }
@@ -109,6 +112,12 @@
                     DocumentProto proto = toDocumentProto(documentValues[j]);
                     propertyProto.addDocumentValues(proto);
                 }
+            } else if (property instanceof EmbeddingVector[]) {
+                EmbeddingVector[] embeddingValues = (EmbeddingVector[]) property;
+                for (int j = 0; j < embeddingValues.length; j++) {
+                    propertyProto.addVectorValues(
+                            embeddingVectorToVectorProto(embeddingValues[j]));
+                }
             } else if (property == null) {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" doesn't have any value!", name));
@@ -205,6 +214,13 @@
                             schemaTypeMap, config);
                 }
                 documentBuilder.setPropertyDocument(name, values);
+            } else if (property.getVectorValuesCount() > 0) {
+                EmbeddingVector[] values =
+                        new EmbeddingVector[property.getVectorValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = vectorProtoToEmbeddingVector(property.getVectorValues(j));
+                }
+                documentBuilder.setPropertyEmbedding(name, values);
             } else {
                 // TODO(b/184966497): Optimize by caching PropertyConfigProto
                 SchemaTypeConfigProto schema =
@@ -216,6 +232,37 @@
     }
 
     /**
+     * Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}.
+     */
+    @NonNull
+    public static EmbeddingVector vectorProtoToEmbeddingVector(
+            @NonNull PropertyProto.VectorProto vectorProto) {
+        Preconditions.checkNotNull(vectorProto);
+
+        float[] values = new float[vectorProto.getValuesCount()];
+        for (int i = 0; i < vectorProto.getValuesCount(); i++) {
+            values[i] = vectorProto.getValues(i);
+        }
+        return new EmbeddingVector(values, vectorProto.getModelSignature());
+    }
+
+    /**
+     * Converts an {@link EmbeddingVector} into a {@link PropertyProto.VectorProto}.
+     */
+    @NonNull
+    public static PropertyProto.VectorProto embeddingVectorToVectorProto(
+            @NonNull EmbeddingVector embedding) {
+        Preconditions.checkNotNull(embedding);
+
+        PropertyProto.VectorProto.Builder builder = PropertyProto.VectorProto.newBuilder();
+        for (int i = 0; i < embedding.getValues().length; i++) {
+            builder.addValues(embedding.getValues()[i]);
+        }
+        builder.setModelSignature(embedding.getModelSignature());
+        return builder.build();
+    }
+
+    /**
      * Get the list of unprefixed parent type names of {@code prefixedSchemaType}.
      *
      * <p>It's guaranteed that child types always appear before parent types in the list.
@@ -305,6 +352,9 @@
             case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
                 documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
                 break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING:
+                documentBuilder.setPropertyEmbedding(propertyName, EMPTY_EMBEDDING_ARRAY);
+                break;
             default:
                 throw new IllegalStateException("Unknown type of value: " + propertyName);
         }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
index 2106917..eedbb0d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
@@ -35,7 +35,7 @@
     private ResultCodeToProtoConverter() {}
 
     /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
-    public static @AppSearchResult.ResultCode int toResultCode(
+    @AppSearchResult.ResultCode public static int toResultCode(
             @NonNull StatusProto.Code statusCode) {
         switch (statusCode) {
             case OK:
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index e21213f..5014f04 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -24,6 +24,7 @@
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
 import com.google.android.icing.proto.IntegerIndexingConfig;
 import com.google.android.icing.proto.JoinableConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
@@ -55,6 +56,7 @@
         Preconditions.checkNotNull(schema);
         SchemaTypeConfigProto.Builder protoBuilder = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType(schema.getSchemaType())
+                .setDescription(schema.getDescription())
                 .setVersion(version);
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
@@ -70,7 +72,8 @@
             @NonNull AppSearchSchema.PropertyConfig property) {
         Preconditions.checkNotNull(property);
         PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
-                .setPropertyName(property.getName());
+                .setPropertyName(property.getName())
+                .setDescription(property.getDescription());
 
         // Set dataType
         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -101,11 +104,6 @@
                         .setValueType(
                                 convertJoinableValueTypeToProto(
                                         stringProperty.getJoinableValueType()))
-                        // @exportToFramework:startStrip()
-                        // Do not call this in framework as it will populate the proto field and
-                        // fail comparison tests.
-                        .setPropagateDelete(stringProperty.getDeletionPropagation())
-                        // @exportToFramework:endStrip()
                         .build();
                 builder.setJoinableConfig(joinableConfig);
             }
@@ -140,6 +138,20 @@
                         .build();
                 builder.setIntegerIndexingConfig(integerIndexingConfig);
             }
+        } else if (property instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            AppSearchSchema.EmbeddingPropertyConfig embeddingProperty =
+                    (AppSearchSchema.EmbeddingPropertyConfig) property;
+            // Set embedding indexing config only if it is indexable (i.e. not INDEXING_TYPE_NONE).
+            // Non-indexable embedding property only requires to builder.setDataType, without the
+            // need to set an EmbeddingIndexingConfig.
+            if (embeddingProperty.getIndexingType()
+                    != AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE) {
+                EmbeddingIndexingConfig embeddingIndexingConfig =
+                        EmbeddingIndexingConfig.newBuilder().setEmbeddingIndexingType(
+                                convertEmbeddingIndexingTypeToProto(
+                                        embeddingProperty.getIndexingType())).build();
+                builder.setEmbeddingIndexingConfig(embeddingIndexingConfig);
+            }
         }
         return builder.build();
     }
@@ -154,6 +166,7 @@
         Preconditions.checkNotNull(proto);
         AppSearchSchema.Builder builder =
                 new AppSearchSchema.Builder(proto.getSchemaType());
+        builder.setDescription(proto.getDescription());
         List<PropertyConfigProto> properties = proto.getPropertiesList();
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -177,20 +190,26 @@
                 return toLongPropertyConfig(proto);
             case DOUBLE:
                 return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case BOOLEAN:
                 return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case BYTES:
                 return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case DOCUMENT:
                 return toDocumentPropertyConfig(proto);
+            case VECTOR:
+                return toEmbeddingPropertyConfig(proto);
             default:
-                throw new IllegalArgumentException("Invalid dataType: " + proto.getDataType());
+                throw new IllegalArgumentException(
+                        "Invalid dataType code: " + proto.getDataType().getNumber());
         }
     }
 
@@ -199,11 +218,11 @@
             @NonNull PropertyConfigProto proto) {
         AppSearchSchema.StringPropertyConfig.Builder builder =
                 new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setJoinableValueType(
                                 convertJoinableValueTypeFromProto(
                                         proto.getJoinableConfig().getValueType()))
-                        .setDeletionPropagation(proto.getJoinableConfig().getPropagateDelete())
                         .setTokenizerType(
                                 proto.getStringIndexingConfig().getTokenizerType().getNumber());
 
@@ -220,6 +239,7 @@
         AppSearchSchema.DocumentPropertyConfig.Builder builder =
                 new AppSearchSchema.DocumentPropertyConfig.Builder(
                                 proto.getPropertyName(), proto.getSchemaType())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setShouldIndexNestedProperties(
                                 proto.getDocumentIndexingConfig().getIndexNestedProperties());
@@ -233,6 +253,7 @@
             @NonNull PropertyConfigProto proto) {
         AppSearchSchema.LongPropertyConfig.Builder builder =
                 new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber());
 
         // Set indexingType
@@ -244,6 +265,21 @@
     }
 
     @NonNull
+    private static AppSearchSchema.EmbeddingPropertyConfig toEmbeddingPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        AppSearchSchema.EmbeddingPropertyConfig.Builder builder =
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber());
+
+        // Set indexingType
+        EmbeddingIndexingConfig.EmbeddingIndexingType.Code embeddingIndexingType =
+                proto.getEmbeddingIndexingConfig().getEmbeddingIndexingType();
+        builder.setIndexingType(convertEmbeddingIndexingTypeFromProto(embeddingIndexingType));
+
+        return builder.build();
+    }
+
+    @NonNull
     private static JoinableConfig.ValueType.Code convertJoinableValueTypeToProto(
             @AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType) {
         switch (joinableValueType) {
@@ -265,12 +301,11 @@
                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
             case QUALIFIED_ID:
                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
-            default:
-                // Avoid crashing in the 'read' path; we should try to interpret the document to the
-                // extent possible.
-                Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
-                return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
         }
+        // Avoid crashing in the 'read' path; we should try to interpret the document to the
+        // extent possible.
+        Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
+        return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
     }
 
     @NonNull
@@ -344,4 +379,34 @@
                 return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
         }
     }
+
+    @NonNull
+    private static EmbeddingIndexingConfig.EmbeddingIndexingType.Code
+            convertEmbeddingIndexingTypeToProto(
+            @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+        switch (indexingType) {
+            case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE:
+                return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.UNKNOWN;
+            case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY:
+                return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.LINEAR_SEARCH;
+            default:
+                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+        }
+    }
+
+    @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+    private static int convertEmbeddingIndexingTypeFromProto(
+            @NonNull EmbeddingIndexingConfig.EmbeddingIndexingType.Code indexingType) {
+        switch (indexingType) {
+            case UNKNOWN:
+                return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+            case LINEAR_SEARCH:
+                return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY;
+            default:
+                // Avoid crashing in the 'read' path; we should try to interpret the document to the
+                // extent possible.
+                Log.w(TAG, "Invalid indexingType: " + indexingType.getNumber());
+                return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+        }
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 8652cca..b88eccd 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -20,8 +20,6 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
-import android.os.Bundle;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchResult;
@@ -30,7 +28,7 @@
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfig;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.localstorage.SchemaCache;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -39,6 +37,7 @@
 import com.google.android.icing.proto.SnippetProto;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -55,24 +54,20 @@
      * Translate a {@link SearchResultProto} into {@link SearchResultPage}.
      *
      * @param proto         The {@link SearchResultProto} containing results.
-     * @param schemaMap     The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
-     *                      stores all existing prefixed schema type.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      * @return {@link SearchResultPage} of results.
      */
     @NonNull
     public static SearchResultPage toSearchResultPage(@NonNull SearchResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
-            @NonNull AppSearchConfig config)
+            @NonNull SchemaCache schemaCache, @NonNull AppSearchConfig config)
             throws AppSearchException {
-        Bundle bundle = new Bundle();
-        bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
-        ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
+        List<SearchResult> results = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap, config);
-            resultBundles.add(result.getBundle());
+            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaCache,
+                    config);
+            results.add(result);
         }
-        bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
-        return new SearchResultPage(bundle);
+        return new SearchResultPage(proto.getNextPageToken(), results);
     }
 
     /**
@@ -80,26 +75,28 @@
      * database prefix will be removed from {@link GenericDocument}.
      *
      * @param proto          The proto to be converted.
-     * @param schemaMap      The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
-     *                       stores all existing prefixed schema type.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      * @return A {@link SearchResult}.
      */
     @NonNull
     private static SearchResult toUnprefixedSearchResult(
             @NonNull SearchResultProto.ResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull AppSearchConfig config) throws AppSearchException {
 
         DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
         String prefix = removePrefixesFromDocument(documentBuilder);
         Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                Preconditions.checkNotNull(schemaMap.get(prefix));
+                schemaCache.getSchemaMapForPrefix(prefix);
         GenericDocument document =
                 GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
                         schemaTypeMap, config);
         SearchResult.Builder builder =
                 new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
                         .setGenericDocument(document).setRankingSignal(proto.getScore());
+        for (int i = 0; i < proto.getAdditionalScoresCount(); i++) {
+            builder.addInformationalRankingSignal(proto.getAdditionalScores(i));
+        }
         if (proto.hasSnippet()) {
             for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
                 SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i);
@@ -118,7 +115,8 @@
                         "Nesting joined results within joined results not allowed.");
             }
 
-            builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap, config));
+            builder.addJoinedResult(
+                    toUnprefixedSearchResult(joinedResultProto, schemaCache, config));
         }
         return builder.build();
     }
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 42cfd44..ad5be47 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
@@ -26,11 +26,14 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.FeatureConstants;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.IcingOptionsConfig;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
@@ -92,11 +95,9 @@
     private final Map<String, Set<String>> mNamespaceMap;
 
     /**
-     *The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>} stores all
-     * prefixed schema filters which are stored inAppSearch. This is a field so that we can
-     * generated nested protos.
+     * The SchemaCache instance held in AppSearch.
      */
-    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap;
+    private final SchemaCache mSchemaCache;
 
     /**
      * Optional config flags in {@link SearchSpecProto}.
@@ -120,21 +121,20 @@
      *                           database prefixes are allowed, so nothing will be searched.
      * @param namespaceMap  The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
      *                      all prefixed namespace filters which are stored in AppSearch.
-     * @param schemaMap     The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
-     *                      stores all prefixed schema filters which are stored inAppSearch.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      */
     public SearchSpecToProtoConverter(
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
             @NonNull Set<String> allAllowedPrefixes,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull IcingOptionsConfig icingOptionsConfig) {
         mQueryExpression = Preconditions.checkNotNull(queryExpression);
         mSearchSpec = Preconditions.checkNotNull(searchSpec);
         mAllAllowedPrefixes = Preconditions.checkNotNull(allAllowedPrefixes);
         mNamespaceMap = Preconditions.checkNotNull(namespaceMap);
-        mSchemaMap = Preconditions.checkNotNull(schemaMap);
+        mSchemaCache = Preconditions.checkNotNull(schemaCache);
         mIcingOptionsConfig = Preconditions.checkNotNull(icingOptionsConfig);
 
         // This field holds the prefix filters for the SearchSpec currently being handled, which
@@ -169,7 +169,7 @@
         if (!mTargetPrefixedNamespaceFilters.isEmpty()) {
             mTargetPrefixedSchemaFilters =
                     SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
-                            mCurrentSearchSpecPrefixFilters, schemaMap,
+                            mCurrentSearchSpecPrefixFilters, schemaCache,
                             searchSpec.getFilterSchemas());
         } else {
             mTargetPrefixedSchemaFilters = new ArraySet<>();
@@ -185,17 +185,17 @@
                 joinSpec.getNestedSearchSpec(),
                 mAllAllowedPrefixes,
                 namespaceMap,
-                schemaMap,
+                schemaCache,
                 mIcingOptionsConfig);
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
+     * Returns whether this search's target filters are empty. If any target filter is empty, we
      * should skip send request to Icing.
      *
-     * <p> The nestedConverter is not checked as {@link SearchResult}s from the nested query have
-     * to be joined to a {@link SearchResult} from the parent query. If the parent query has
-     * nothing to search, then so does the child query.
+     * <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have to
+     * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to
+     * search, then so does the child query.
      */
     public boolean hasNothingToSearch() {
         return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
@@ -291,6 +291,13 @@
                 .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
                 .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch());
 
+        List<EmbeddingVector> searchEmbeddings = mSearchSpec.getSearchEmbeddings();
+        for (int i = 0; i < searchEmbeddings.size(); i++) {
+            protoBuilder.addEmbeddingQueryVectors(
+                    GenericDocumentToProtoConverter.embeddingVectorToVectorProto(
+                            searchEmbeddings.get(i)));
+        }
+
         // Convert type property filter map into type property mask proto.
         for (Map.Entry<String, List<String>> entry :
                 mSearchSpec.getFilterProperties().entrySet()) {
@@ -319,11 +326,22 @@
         }
         protoBuilder.setTermMatchType(termMatchCodeProto);
 
+        @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType =
+                mSearchSpec.getDefaultEmbeddingSearchMetricType();
+        SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto =
+                SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType);
+        if (embeddingSearchMetricTypeProto == null || embeddingSearchMetricTypeProto.equals(
+                SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) {
+            throw new IllegalArgumentException(
+                    "Invalid embedding search metric type: " + embeddingSearchMetricType);
+        }
+        protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto);
+
         if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) {
             JoinSpecProto.NestedSpecProto nestedSpec =
                     JoinSpecProto.NestedSpecProto.newBuilder()
                             .setResultSpec(mNestedConverter.toResultSpecProto(
-                                    mNamespaceMap, mSchemaMap))
+                                    mNamespaceMap, mSchemaCache))
                             .setScoringSpec(mNestedConverter.toScoringSpecProto())
                             .setSearchSpec(mNestedConverter.toSearchSpecProto())
                             .build();
@@ -342,18 +360,18 @@
             protoBuilder.setJoinSpec(joinSpecProtoBuilder);
         }
 
-        // TODO(b/208654892) Remove this field once EXPERIMENTAL_ICING_ADVANCED_QUERY is fully
-        //  supported.
-        boolean turnOnIcingAdvancedQuery =
-                mSearchSpec.isNumericSearchEnabled() || mSearchSpec.isVerbatimSearchEnabled()
-                        || mSearchSpec.isListFilterQueryLanguageEnabled();
-        if (turnOnIcingAdvancedQuery) {
-            protoBuilder.setSearchType(
-                    SearchSpecProto.SearchType.Code.EXPERIMENTAL_ICING_ADVANCED_QUERY);
+        if (mSearchSpec.isListFilterHasPropertyFunctionEnabled()
+                && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) {
+            // This condition should never be reached as long as Features.isFeatureSupported() is
+            // consistent with IcingOptionsConfig.
+            throw new UnsupportedOperationException(
+                    FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                            + " is currently not operational because the building process for the "
+                            + "associated metadata has not yet been turned on.");
         }
 
         // Set enabled features
-        protoBuilder.addAllEnabledFeatures(mSearchSpec.getEnabledFeatures());
+        protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(mSearchSpec.getEnabledFeatures()));
 
         return protoBuilder.build();
     }
@@ -390,14 +408,12 @@
      *
      * @param namespaceMap    The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
      *                        all existing prefixed namespace.
-     * @param schemaMap       The cached Map of {@code <Prefix, Map<PrefixedSchemaType,
-     *                        schemaProto>>} stores all prefixed schema filters which are stored
-     *                        inAppSearch.
+     * @param schemaCache     The SchemaCache instance held in AppSearch.
      */
     @NonNull
     public ResultSpecProto toResultSpecProto(
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull SchemaCache schemaCache) {
         ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
                 .setNumPerPage(mSearchSpec.getResultCountPerPage())
                 .setSnippetSpec(
@@ -429,7 +445,7 @@
                 break;
             case SearchSpec.GROUPING_TYPE_PER_SCHEMA:
                 addPerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
-                        mSearchSpec.getResultGroupingLimit(), schemaMap, resultSpecBuilder);
+                        mSearchSpec.getResultGroupingLimit(), schemaCache, resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
                 break;
             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE:
@@ -441,20 +457,20 @@
             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
                 addPerPackagePerSchemaResultGroupings(mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
-                        schemaMap, resultSpecBuilder);
+                        schemaCache, resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
                 break;
             case SearchSpec.GROUPING_TYPE_PER_NAMESPACE | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
                 addPerNamespaceAndSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
-                        namespaceMap, schemaMap, resultSpecBuilder);
+                        namespaceMap, schemaCache, resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
                 break;
             case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE
                 | SearchSpec.GROUPING_TYPE_PER_SCHEMA:
                 addPerPackagePerNamespacePerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
-                        namespaceMap, schemaMap, resultSpecBuilder);
+                        namespaceMap, schemaCache, resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
                 break;
             default:
@@ -468,7 +484,7 @@
         // Rewrite filters to include a database prefix.
         for (int i = 0; i < typePropertyMaskBuilders.size(); i++) {
             String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();
-            if (unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD)) {
+            if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
                 resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build());
             } else {
                 // Qualify the given schema types
@@ -502,6 +518,8 @@
         addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder);
 
         protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression());
+        protoBuilder.addAllAdditionalAdvancedScoringExpressions(
+                mSearchSpec.getInformationalRankingExpressions());
 
         return protoBuilder.build();
     }
@@ -536,6 +554,26 @@
     }
 
     /**
+     * Maps a list of AppSearch search feature strings to the list of the corresponding Icing
+     * feature strings.
+     *
+     * @param appSearchFeatures The list of AppSearch search feature strings.
+     */
+    @NonNull
+    private static List<String> toIcingSearchFeatures(@NonNull List<String> appSearchFeatures) {
+        List<String> result = new ArrayList<>();
+        for (int i = 0; i < appSearchFeatures.size(); i++) {
+            String appSearchFeature = appSearchFeatures.get(i);
+            if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) {
+                result.add("HAS_PROPERTY_FUNCTION");
+            } else {
+                result.add(appSearchFeature);
+            }
+        }
+        return result;
+    }
+
+    /**
      * Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the
      * same as the list of namespaces. If a namespace exists under different packages and/or
      * different databases, they should still be grouped together.
@@ -593,7 +631,7 @@
             String packageName = getPackageName(prefix);
             // Create a new prefix without the database name. This will allow us to group namespaces
             // that have the same name and package but a different database name together.
-            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+            String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */"");
             for (String prefixedNamespace : prefixedNamespaces) {
                 String namespace;
                 try {
@@ -623,17 +661,15 @@
      * different databases, they should still be grouped together.
      *
      * @param prefixes      Prefixes that we should prepend to all our filters.
-     * @param schemaMap     The schema map contains all prefixed existing schema types.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      */
     private static Map<String, List<String>> getSchemaToPrefixedSchemas(
             @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull SchemaCache schemaCache) {
         Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>();
         for (String prefix : prefixes) {
-            Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
-            if (prefixedSchemas == null) {
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemas =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             for (String prefixedSchema : prefixedSchemas.keySet()) {
                 String schema;
                 try {
@@ -661,17 +697,15 @@
      * schema, then those should be grouped together.
      *
      * @param prefixes      Prefixes that we should prepend to all our filters.
-     * @param schemaMap     The schema map contains all prefixed existing schema types.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      */
     private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas(
             @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull SchemaCache schemaCache) {
         Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>();
         for (String prefix : prefixes) {
-            Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
-            if (prefixedSchemas == null) {
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemas =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             String packageName = getPackageName(prefix);
             // Create a new prefix without the database name. This will allow us to group schemas
             // that have the same name and package but a different database name together.
@@ -733,16 +767,16 @@
      *
      * @param prefixes          Prefixes that we should prepend to all our filters.
      * @param maxNumResults     The maximum number of results for each grouping to support.
-     * @param schemaMap         The schema map contains all prefixed existing schema types.
+     * @param schemaCache       The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpecs as a specified by client.
      */
     private static void addPerPackagePerSchemaResultGroupings(
             @NonNull Set<String> prefixes,
             int maxNumResults,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> packageAndSchemaToSchemas =
-                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
             List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -764,19 +798,19 @@
      * @param prefixes          Prefixes that we should prepend to all our filters.
      * @param maxNumResults     The maximum number of results for each grouping to support.
      * @param namespaceMap      The namespace map contains all prefixed existing namespaces.
-     * @param schemaMap         The schema map contains all prefixed existing schema types.
+     * @param schemaCache   The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerPackagePerNamespacePerSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> packageAndNamespaceToNamespaces =
                 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
         Map<String, List<String>> packageAndSchemaToSchemas =
-                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
             for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
@@ -884,16 +918,16 @@
      *
      * @param prefixes          Prefixes that we should prepend to all our filters.
      * @param maxNumResults     The maximum number of results for each grouping to support.
-     * @param schemaMap         The schema map contains all prefixed existing schema types.
+     * @param schemaCache       The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> schemaToPrefixedSchemas =
-                getSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
             List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -915,19 +949,19 @@
      * @param prefixes          Prefixes that we should prepend to all our filters.
      * @param maxNumResults     The maximum number of results for each grouping to support.
      * @param namespaceMap      The namespace map contains all prefixed existing namespaces.
-     * @param schemaMap         The schema map contains all prefixed existing schema types.
+     * @param schemaCache       The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerNamespaceAndSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> namespaceToPrefixedNamespaces =
                 getNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
         Map<String, List<String>> schemaToPrefixedSchemas =
-                getSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
             for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
index 9015102..d3b7687 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterUtil.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.collection.ArraySet;
 
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -74,33 +75,26 @@
      * intersection set with those prefixed schema candidates that are stored in AppSearch.
      *
      * @param prefixes              Set of database prefix which the caller want to access.
-     * @param schemaMap             The cached Map of
-     *                              {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
-     *                              stores all prefixed schema filters which are stored in
-     *                              AppSearch.
+     * @param schemaCache           The SchemaCache instance held in AppSearch.
      * @param inputSchemaFilters    The set contains all desired but un-prefixed namespace filters
      *                              of user. If the inputSchemaFilters is empty, all existing
      *                              prefixedCandidates will be added to the prefixedTargetFilters.
      */
     static Set<String> generateTargetSchemaFilters(
             @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull List<String> inputSchemaFilters) {
         Set<String> targetPrefixedSchemaFilters = new ArraySet<>();
         // Append prefix to input schema filters and get the intersection of existing schema filter.
         for (String prefix : prefixes) {
             // Step1: find all prefixed schema candidates that are stored in AppSearch.
-            Map<String, SchemaTypeConfigProto> prefixedSchemaMap = schemaMap.get(prefix);
-            if (prefixedSchemaMap == null) {
-                // This is should never happen. All prefixes should be verified before reach
-                // here.
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemaMap =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             Set<String> prefixedSchemaCandidates = prefixedSchemaMap.keySet();
-            // Step2: get the intersection of user searching filters and those candidates which are
-            // stored in AppSearch.
-            addIntersectedFilters(prefix, prefixedSchemaCandidates, inputSchemaFilters,
-                    targetPrefixedSchemaFilters);
+            // Step2: get the intersection of user searching filters (after polymorphism
+            // expansion) and those candidates which are stored in AppSearch.
+            addIntersectedPolymorphicSchemaFilters(prefix, prefixedSchemaCandidates,
+                    schemaCache, inputSchemaFilters, targetPrefixedSchemaFilters);
         }
         return targetPrefixedSchemaFilters;
     }
@@ -136,4 +130,45 @@
             }
         }
     }
+
+    /**
+     * Find the schema intersection set of candidates existing in AppSearch and user specified
+     * schema filters after polymorphism expansion.
+     *
+     * @param prefix                The package and database's identifier.
+     * @param prefixedCandidates    The set contains all prefixed candidates which are existing
+     *                              in a database.
+     * @param schemaCache           The SchemaCache instance held in AppSearch.
+     * @param inputFilters          The set contains all desired but un-prefixed filters of user.
+     *                              If the inputFilters is empty, all prefixedCandidates will be
+     *                              added to the prefixedTargetFilters.
+     * @param prefixedTargetFilters The output set contains all desired prefixed filters which
+     *                              are existing in the database.
+     */
+    private static void addIntersectedPolymorphicSchemaFilters(
+            @NonNull String prefix,
+            @NonNull Set<String> prefixedCandidates,
+            @NonNull SchemaCache schemaCache,
+            @NonNull List<String> inputFilters,
+            @NonNull Set<String> prefixedTargetFilters) {
+        if (inputFilters.isEmpty()) {
+            // Client didn't specify certain schemas to search over, add all candidates.
+            // Polymorphism expansion is not necessary here, since expanding the set of all
+            // schema types will result in the same set of schema types.
+            prefixedTargetFilters.addAll(prefixedCandidates);
+            return;
+        }
+
+        Set<String> currentPrefixedTargetFilters = new ArraySet<>();
+        for (int i = 0; i < inputFilters.size(); i++) {
+            String prefixedTargetSchemaFilter = prefix + inputFilters.get(i);
+            if (prefixedCandidates.contains(prefixedTargetSchemaFilter)) {
+                currentPrefixedTargetFilters.add(prefixedTargetSchemaFilter);
+            }
+        }
+        // Expand schema filters by polymorphism.
+        currentPrefixedTargetFilters = schemaCache.getSchemaTypesWithDescendants(prefix,
+                currentPrefixedTargetFilters);
+        prefixedTargetFilters.addAll(currentPrefixedTargetFilters);
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
index 52f57bd..7c99bb2 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
@@ -19,10 +19,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.localstorage.SchemaCache;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.NamespaceDocumentUriGroup;
-import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SuggestionScoringSpecProto;
 import com.google.android.icing.proto.SuggestionSpecProto;
 import com.google.android.icing.proto.TermMatchType;
@@ -73,7 +73,7 @@
             @NonNull SearchSuggestionSpec searchSuggestionSpec,
             @NonNull Set<String> prefixes,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull SchemaCache schemaCache) {
         mSuggestionQueryExpression = Preconditions.checkNotNull(suggestionQueryExpression);
         mSearchSuggestionSpec = Preconditions.checkNotNull(searchSuggestionSpec);
         mPrefixes = Preconditions.checkNotNull(prefixes);
@@ -83,11 +83,11 @@
                         prefixes, namespaceMap, searchSuggestionSpec.getFilterNamespaces());
         mTargetPrefixedSchemaFilters =
                 SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
-                        prefixes, schemaMap, searchSuggestionSpec.getFilterSchemas());
+                        prefixes, schemaCache, searchSuggestionSpec.getFilterSchemas());
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
+     * Returns whether this search's target filters are empty. If any target filter is empty, we
      * should skip send request to Icing.
      */
     public boolean hasNothingToSearch() {
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 02d2971..5f28ea0 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
@@ -45,6 +45,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class CallStats {
+    /** Call types. */
     @IntDef(value = {
             CALL_TYPE_UNKNOWN,
             CALL_TYPE_INITIALIZE,
@@ -77,6 +78,7 @@
             CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
             CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
             CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+            CALL_TYPE_EXECUTE_APP_FUNCTION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CallType {
@@ -113,6 +115,7 @@
     public static final int CALL_TYPE_REGISTER_OBSERVER_CALLBACK = 28;
     public static final int CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK = 29;
     public static final int CALL_TYPE_GLOBAL_GET_NEXT_PAGE = 30;
+    public static final int CALL_TYPE_EXECUTE_APP_FUNCTION = 31;
 
     // These strings are for the subset of call types that correspond to an AppSearchManager API
     private static final String CALL_TYPE_STRING_INITIALIZE = "initialize";
@@ -144,6 +147,7 @@
     private static final String CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK =
             "globalUnregisterObserverCallback";
     private static final String CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE = "globalGetNextPage";
+    private static final String CALL_TYPE_STRING_EXECUTE_APP_FUNCTION = "executeAppFunction";
 
     @Nullable
     private final String mPackageName;
@@ -410,6 +414,8 @@
                 return CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK;
             case CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE:
                 return CALL_TYPE_GLOBAL_GET_NEXT_PAGE;
+            case CALL_TYPE_STRING_EXECUTE_APP_FUNCTION:
+                return CALL_TYPE_EXECUTE_APP_FUNCTION;
             default:
                 return CALL_TYPE_UNKNOWN;
         }
@@ -445,6 +451,7 @@
                 CALL_TYPE_GET_STORAGE_INFO,
                 CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
                 CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
-                CALL_TYPE_GLOBAL_GET_NEXT_PAGE));
+                CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+                CALL_TYPE_EXECUTE_APP_FUNCTION));
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
new file mode 100644
index 0000000..4477ce3
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a click action, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ClickStats {
+    private final long mTimestampMillis;
+
+    private final long mTimeStayOnResultMillis;
+
+    private final int mResultRankInBlock;
+
+    private final int mResultRankGlobal;
+
+    private final boolean mIsGoodClick;
+
+    ClickStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mTimestampMillis = builder.mTimestampMillis;
+        mTimeStayOnResultMillis = builder.mTimeStayOnResultMillis;
+        mResultRankInBlock = builder.mResultRankInBlock;
+        mResultRankGlobal = builder.mResultRankGlobal;
+        mIsGoodClick = builder.mIsGoodClick;
+    }
+
+    /** Returns the click action timestamp in milliseconds since Unix epoch. */
+    public long getTimestampMillis() {
+        return mTimestampMillis;
+    }
+
+    /** Returns the time (duration) of the user staying on the clicked result. */
+    public long getTimeStayOnResultMillis() {
+        return mTimeStayOnResultMillis;
+    }
+
+    /** Returns the in-block rank of the clicked result. */
+    public int getResultRankInBlock() {
+        return mResultRankInBlock;
+    }
+
+    /** Returns the global rank of the clicked result. */
+    public int getResultRankGlobal() {
+        return mResultRankGlobal;
+    }
+
+    /**
+     * Returns whether this click is a good click or not.
+     *
+     * @see Builder#setIsGoodClick
+     */
+    public boolean isGoodClick() {
+        return mIsGoodClick;
+    }
+
+    /** Builder for {@link ClickStats} */
+    public static final class Builder {
+        private long mTimestampMillis;
+
+        private long mTimeStayOnResultMillis;
+
+        private int mResultRankInBlock;
+
+        private int mResultRankGlobal;
+
+        private boolean mIsGoodClick = true;
+
+        /** Sets the click action timestamp in milliseconds since Unix epoch. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimestampMillis(long timestampMillis) {
+            mTimestampMillis = timestampMillis;
+            return this;
+        }
+
+        /** Sets the time (duration) of the user staying on the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+            mTimeStayOnResultMillis = timeStayOnResultMillis;
+            return this;
+        }
+
+        /** Sets the in-block rank of the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankInBlock(int resultRankInBlock) {
+            mResultRankInBlock = resultRankInBlock;
+            return this;
+        }
+
+        /** Sets the global rank of the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankGlobal(int resultRankGlobal) {
+            mResultRankGlobal = resultRankGlobal;
+            return this;
+        }
+
+        /**
+         * Sets the flag indicating whether the click is good or not.
+         *
+         * <p>A good click means the user is satisfied by the clicked document. The caller should
+         * define its own criteria and set this field accordingly.
+         *
+         * <p>The default value is true if unset. We should treat it as a good click by default if
+         * the caller didn't specify or could not determine for several reasons:
+         *
+         * <ul>
+         *   <li>It may be difficult for the caller to determine if the user is satisfied by the
+         *       clicked document or not.
+         *   <li>AppSearch collects search quality metrics that are related to number of good
+         *       clicks. We don't want to demote the quality score aggressively by the undetermined
+         *       ones.
+         * </ul>
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setIsGoodClick(boolean isGoodClick) {
+            mIsGoodClick = isGoodClick;
+            return this;
+        }
+
+        /** Builds a new {@link ClickStats} from the {@link ClickStats.Builder}. */
+        @NonNull
+        public ClickStats build() {
+            return new ClickStats(/* builder= */ this);
+        }
+    }
+}
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 73a8c96..d650e7d 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
@@ -37,6 +37,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class RemoveStats {
+    /** Types of stats available for remove API. */
     @IntDef(value = {
             // It needs to be sync with DeleteType.Code in
             // external/icing/proto/icing/proto/logging.proto#DeleteStatsProto
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
new file mode 100644
index 0000000..0886d2f
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+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;
+import java.util.List;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search intent, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * A search intent includes a valid AppSearch search request, potentially followed by several user
+ * click actions (see {@link ClickStats}) on fetched result documents. Related information of a
+ * search intent will be extracted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchIntentStats {
+    /** AppSearch query correction type compared with the previous query. */
+    @IntDef(value = {
+            QUERY_CORRECTION_TYPE_UNKNOWN,
+            QUERY_CORRECTION_TYPE_FIRST_QUERY,
+            QUERY_CORRECTION_TYPE_REFINEMENT,
+            QUERY_CORRECTION_TYPE_ABANDONMENT,
+            QUERY_CORRECTION_TYPE_END_SESSION,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface QueryCorrectionType {
+    }
+
+    public static final int QUERY_CORRECTION_TYPE_UNKNOWN = 0;
+
+    public static final int QUERY_CORRECTION_TYPE_FIRST_QUERY = 1;
+
+    public static final int QUERY_CORRECTION_TYPE_REFINEMENT = 2;
+
+    public static final int QUERY_CORRECTION_TYPE_ABANDONMENT = 3;
+
+    public static final int QUERY_CORRECTION_TYPE_END_SESSION = 4;
+
+    @NonNull
+    private final String mPackageName;
+
+    @Nullable
+    private final String mDatabase;
+
+    @Nullable
+    private final String mPrevQuery;
+
+    @Nullable
+    private final String mCurrQuery;
+
+    private final long mTimestampMillis;
+
+    private final int mNumResultsFetched;
+
+    @QueryCorrectionType
+    private final int mQueryCorrectionType;
+
+    @NonNull
+    private final List<ClickStats> mClicksStats;
+
+    SearchIntentStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mPrevQuery = builder.mPrevQuery;
+        mCurrQuery = builder.mCurrQuery;
+        mTimestampMillis = builder.mTimestampMillis;
+        mNumResultsFetched = builder.mNumResultsFetched;
+        mQueryCorrectionType = builder.mQueryCorrectionType;
+        mClicksStats = builder.mClicksStats;
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns calling database name.
+     *
+     * <p>For global search, database name will be null.
+     */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns the raw query string of the previous search intent. */
+    @Nullable
+    public String getPrevQuery() {
+        return mPrevQuery;
+    }
+
+    /** Returns the raw query string of this (current) search intent. */
+    @Nullable
+    public String getCurrQuery() {
+        return mCurrQuery;
+    }
+
+    /** Returns the search intent timestamp in milliseconds since Unix epoch. */
+    public long getTimestampMillis() {
+        return mTimestampMillis;
+    }
+
+    /**
+     * Returns total number of results fetched from AppSearch by the client in this search intent.
+     */
+    public int getNumResultsFetched() {
+        return mNumResultsFetched;
+    }
+
+    /**
+     * Returns the correction type of the query in this search intent compared with the previous
+     * search intent. Default value: {@link SearchIntentStats#QUERY_CORRECTION_TYPE_UNKNOWN}.
+     */
+    @QueryCorrectionType
+    public int getQueryCorrectionType() {
+        return mQueryCorrectionType;
+    }
+
+    /** Returns the list of {@link ClickStats} in this search intent. */
+    @NonNull
+    public List<ClickStats> getClicksStats() {
+        return mClicksStats;
+    }
+
+    /** Builder for {@link SearchIntentStats} */
+    public static final class Builder {
+        @NonNull
+        private final String mPackageName;
+
+        @Nullable
+        private String mDatabase;
+
+        @Nullable
+        private String mPrevQuery;
+
+        @Nullable
+        private String mCurrQuery;
+
+        private long mTimestampMillis;
+
+        private int mNumResultsFetched;
+
+        @QueryCorrectionType
+        private int mQueryCorrectionType = QUERY_CORRECTION_TYPE_UNKNOWN;
+
+        @NonNull
+        private List<ClickStats> mClicksStats = new ArrayList<>();
+
+        private boolean mBuilt = false;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+        }
+
+        /** Constructor the {@link Builder} from an existing {@link SearchIntentStats}. */
+        public Builder(@NonNull SearchIntentStats searchIntentStats) {
+            Preconditions.checkNotNull(searchIntentStats);
+
+            mPackageName = searchIntentStats.getPackageName();
+            mDatabase = searchIntentStats.getDatabase();
+            mPrevQuery = searchIntentStats.getPrevQuery();
+            mCurrQuery = searchIntentStats.getCurrQuery();
+            mTimestampMillis = searchIntentStats.getTimestampMillis();
+            mNumResultsFetched = searchIntentStats.getNumResultsFetched();
+            mQueryCorrectionType = searchIntentStats.getQueryCorrectionType();
+            mClicksStats.addAll(searchIntentStats.getClicksStats());
+        }
+
+        /**
+         * Sets calling database name.
+         *
+         * <p>For global search, database name will be null.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setDatabase(@Nullable String database) {
+            resetIfBuilt();
+            mDatabase = database;
+            return this;
+        }
+
+        /** Sets the raw query string of the previous search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setPrevQuery(@Nullable String prevQuery) {
+            resetIfBuilt();
+            mPrevQuery = prevQuery;
+            return this;
+        }
+
+        /** Sets the raw query string of this (current) search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setCurrQuery(@Nullable String currQuery) {
+            resetIfBuilt();
+            mCurrQuery = currQuery;
+            return this;
+        }
+
+        /** Sets the search intent timestamp in milliseconds since Unix epoch. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimestampMillis(long timestampMillis) {
+            resetIfBuilt();
+            mTimestampMillis = timestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets total number of results fetched from AppSearch by the client in this search intent.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNumResultsFetched(int numResultsFetched) {
+            resetIfBuilt();
+            mNumResultsFetched = numResultsFetched;
+            return this;
+        }
+
+        /**
+         * Sets the correction type of the query in this search intent compared with the previous
+         * search intent.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQueryCorrectionType(@QueryCorrectionType int queryCorrectionType) {
+            resetIfBuilt();
+            mQueryCorrectionType = queryCorrectionType;
+            return this;
+        }
+
+        /** Adds one or more {@link ClickStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addClicksStats(@NonNull ClickStats... clicksStats) {
+            Preconditions.checkNotNull(clicksStats);
+            resetIfBuilt();
+            return addClicksStats(Arrays.asList(clicksStats));
+        }
+
+        /** Adds a collection of {@link ClickStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addClicksStats(@NonNull Collection<? extends ClickStats> clicksStats) {
+            Preconditions.checkNotNull(clicksStats);
+            resetIfBuilt();
+            mClicksStats.addAll(clicksStats);
+            return this;
+        }
+
+        /**
+         * If built, make a copy of previous data for every field so that the builder can be reused.
+         */
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mClicksStats = new ArrayList<>(mClicksStats);
+                mBuilt = false;
+            }
+        }
+
+        /** Builds a new {@link SearchIntentStats} from the {@link Builder}. */
+        @NonNull
+        public SearchIntentStats build() {
+            mBuilt = true;
+            return new SearchIntentStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java
new file mode 100644
index 0000000..11d8ea3
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchSessionStats.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search session, converted from {@link
+ * androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}. It contains a list of
+ * {@link SearchIntentStats} and aggregated metrics of them.
+ *
+ * <p>A search session is consist of a sequence of related search intents. See {@link
+ * SearchIntentStats} for more details.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchSessionStats {
+    @NonNull private final String mPackageName;
+
+    @Nullable private final String mDatabase;
+
+    @NonNull private final List<SearchIntentStats> mSearchIntentsStats;
+
+    SearchSessionStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mSearchIntentsStats = builder.mSearchIntentsStats;
+    }
+
+    /**
+     * Returns a nullable {@link SearchIntentStats} instance containing information of the last
+     * search intent which ended the search session.
+     *
+     * <p>If {@link #getSearchIntentsStats} is empty (i.e. the caller didn't add any {@link
+     * SearchIntentStats} via {@link Builder#addSearchIntentsStats}), then return null.
+     *
+     * <p>It is similar to the last element in {@link #getSearchIntentsStats}, except there is no
+     * previous query and the query correction type is tagged as {@link
+     * SearchIntentStats#QUERY_CORRECTION_TYPE_END_SESSION}.
+     *
+     * <p>This stats is useful to determine whether the user ended the search session with
+     * satisfaction (i.e. had found desired result documents) or not.
+     */
+    @Nullable
+    public SearchIntentStats getEndSessionSearchIntentStats() {
+        if (mSearchIntentsStats.isEmpty()) {
+            return null;
+        }
+
+        SearchIntentStats lastSearchIntentStats =
+                mSearchIntentsStats.get(mSearchIntentsStats.size() - 1);
+        return new SearchIntentStats.Builder(lastSearchIntentStats)
+                .setPrevQuery(null)
+                .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION)
+                .build();
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns calling database name.
+     *
+     * <p>For global search, database name will be null.
+     */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns the list of {@link SearchIntentStats} in this search session. */
+    @NonNull
+    public List<SearchIntentStats> getSearchIntentsStats() {
+        return mSearchIntentsStats;
+    }
+
+    /** Builder for {@link SearchSessionStats}. */
+    public static final class Builder {
+        @NonNull private final String mPackageName;
+
+        @Nullable private String mDatabase;
+
+        @NonNull private List<SearchIntentStats> mSearchIntentsStats = new ArrayList<>();
+
+        private boolean mBuilt = false;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+        }
+
+        /**
+         * Sets calling database name.
+         *
+         * <p>For global search, database name will be null.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setDatabase(@Nullable String database) {
+            resetIfBuilt();
+            mDatabase = database;
+            return this;
+        }
+
+        /** Adds one or more {@link SearchIntentStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addSearchIntentsStats(@NonNull SearchIntentStats... searchIntentsStats) {
+            Preconditions.checkNotNull(searchIntentsStats);
+            resetIfBuilt();
+            return addSearchIntentsStats(Arrays.asList(searchIntentsStats));
+        }
+
+        /** Adds a collection of {@link SearchIntentStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addSearchIntentsStats(
+                @NonNull Collection<? extends SearchIntentStats> searchIntentsStats) {
+            Preconditions.checkNotNull(searchIntentsStats);
+            resetIfBuilt();
+            mSearchIntentsStats.addAll(searchIntentsStats);
+            return this;
+        }
+
+        /**
+         * If built, make a copy of previous data for every field so that the builder can be reused.
+         */
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mSearchIntentsStats = new ArrayList<>(mSearchIntentsStats);
+                mBuilt = false;
+            }
+        }
+
+        /** Builds a new {@link SearchSessionStats} from the {@link Builder}. */
+        @NonNull
+        public SearchSessionStats build() {
+            mBuilt = true;
+            return new SearchSessionStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
index a87ae04..28d5117 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SearchStats {
+    /** Types of Visibility scopes available for search. */
     @IntDef(value = {
             // Searches apps' own documents.
             VISIBILITY_SCOPE_LOCAL,
@@ -137,6 +138,7 @@
     private final int mNativeNumJoinedResultsCurrentPage;
     /** Time taken to join documents together. */
     private final int mNativeJoinLatencyMillis;
+    @Nullable private final String mSearchSourceLogTag;
 
     SearchStats(@NonNull Builder builder) {
         Preconditions.checkNotNull(builder);
@@ -170,6 +172,7 @@
         mJoinType = builder.mJoinType;
         mNativeNumJoinedResultsCurrentPage = builder.mNativeNumJoinedResultsCurrentPage;
         mNativeJoinLatencyMillis = builder.mNativeJoinLatencyMillis;
+        mSearchSourceLogTag = builder.mSearchSourceLogTag;
     }
 
     /** Returns the package name of the session. */
@@ -342,6 +345,12 @@
         return mNativeJoinLatencyMillis;
     }
 
+    /**  Returns a tag to indicate the source of this search, or {code null} if never set. */
+    @Nullable
+    public String getSearchSourceLogTag() {
+        return mSearchSourceLogTag;
+    }
+
     /** Builder for {@link SearchStats} */
     public static class Builder {
         @NonNull
@@ -377,6 +386,7 @@
         @JoinableValueType int mJoinType;
         int mNativeNumJoinedResultsCurrentPage;
         int mNativeJoinLatencyMillis;
+        @Nullable String mSearchSourceLogTag;
 
         /**
          * Constructor
@@ -604,6 +614,7 @@
         }
 
         /** Sets whether or not this is a join query */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setJoinType(@JoinableValueType int joinType) {
             mJoinType = joinType;
@@ -611,6 +622,7 @@
         }
 
         /** Set the total number of joined documents in a page. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setNativeNumJoinedResultsCurrentPage(int nativeNumJoinedResultsCurrentPage) {
             mNativeNumJoinedResultsCurrentPage = nativeNumJoinedResultsCurrentPage;
@@ -618,12 +630,21 @@
         }
 
         /** Sets time it takes to join documents together in icing. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setNativeJoinLatencyMillis(int nativeJoinLatencyMillis) {
             mNativeJoinLatencyMillis = nativeJoinLatencyMillis;
             return this;
         }
 
+        /** Sets a tag to indicate the source of this search. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setSearchSourceLogTag(@Nullable String searchSourceLogTag) {
+            mSearchSourceLogTag = searchSourceLogTag;
+            return this;
+        }
+
         /**
          * Constructs a new {@link SearchStats} from the contents of this
          * {@link SearchStats.Builder}.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
new file mode 100644
index 0000000..baa3e4e
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ *  <!--@exportToFramework:ifJetpack()-->
+ *  {@link androidx.appsearch.usagereporting.ClickAction}
+ *  <!--@exportToFramework:else()
+ *  click action
+ *  -->
+ * {@link GenericDocument}, which contains getters for click action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ClickActionGenericDocument extends TakenActionGenericDocument {
+    private static final String PROPERTY_PATH_QUERY = "query";
+    private static final String PROPERTY_PATH_RESULT_RANK_IN_BLOCK = "resultRankInBlock";
+    private static final String PROPERTY_PATH_RESULT_RANK_GLOBAL = "resultRankGlobal";
+    private static final String PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS = "timeStayOnResultMillis";
+
+    ClickActionGenericDocument(@NonNull GenericDocument document) {
+        super(Preconditions.checkNotNull(document));
+    }
+
+    /** Returns the string value of property {@code query}. */
+    @Nullable
+    public String getQuery() {
+        return getPropertyString(PROPERTY_PATH_QUERY);
+    }
+
+    /** Returns the integer value of property {@code resultRankInBlock}. */
+    public int getResultRankInBlock() {
+        return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK);
+    }
+
+    /** Returns the integer value of property {@code resultRankGlobal}. */
+    public int getResultRankGlobal() {
+        return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL);
+    }
+
+    /** Returns the long value of property {@code timeStayOnResultMillis}. */
+    public long getTimeStayOnResultMillis() {
+        return getPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS);
+    }
+
+    /** Builder for {@link ClickActionGenericDocument}. */
+    public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+        /**
+         * Creates a new {@link ClickActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace  the namespace to set for the {@link GenericDocument}.
+         * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *                   provided {@code schemaType} must be defined using
+         *                   {@link AppSearchSession#setSchemaAsync} prior
+         *                   to inserting a document of this {@code schemaType} into the
+         *                   AppSearch index using
+         *                   {@link AppSearchSession#putAsync}.
+         *                   Otherwise, the document will be rejected by
+         *                   {@link AppSearchSession#putAsync} with result code
+         *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+                    Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_CLICK);
+        }
+
+        /**
+         * Creates a new {@link ClickActionGenericDocument.Builder} from an existing
+         * {@link GenericDocument}.
+         *
+         * @param document a generic document object.
+         *
+         * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+         *                                  not {@link ActionConstants#ACTION_TYPE_CLICK}.
+         */
+        public Builder(@NonNull GenericDocument document) {
+            super(Preconditions.checkNotNull(document));
+
+            if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+                    != ActionConstants.ACTION_TYPE_CLICK) {
+                throw new IllegalArgumentException(
+                        "Invalid action type for ClickActionGenericDocument");
+            }
+        }
+
+        /**
+         * Sets the string value of property {@code query} by the user-entered search input
+         * (without any operators or rewriting) that yielded the
+         * {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@NonNull String query) {
+            Preconditions.checkNotNull(query);
+            setPropertyString(PROPERTY_PATH_QUERY, query);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code resultRankInBlock} by the rank of the clicked
+         * {@link androidx.appsearch.app.SearchResult} document among the user-defined block.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankInBlock(int resultRankInBlock) {
+            Preconditions.checkArgumentNonnegative(resultRankInBlock);
+            setPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK, resultRankInBlock);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code resultRankGlobal} by the global rank of the
+         * clicked {@link androidx.appsearch.app.SearchResult} document.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankGlobal(int resultRankGlobal) {
+            Preconditions.checkArgumentNonnegative(resultRankGlobal);
+            setPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL, resultRankGlobal);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code timeStayOnResultMillis} by the time in
+         * milliseconds that user stays on the {@link androidx.appsearch.app.SearchResult} document
+         * after clicking it.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+            setPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS, timeStayOnResultMillis);
+            return this;
+        }
+
+        /** Builds a {@link ClickActionGenericDocument}. */
+        @Override
+        @NonNull
+        public ClickActionGenericDocument build() {
+            return new ClickActionGenericDocument(super.build());
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
new file mode 100644
index 0000000..bedbf21
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ *  <!--@exportToFramework:ifJetpack()-->
+ *  {@link androidx.appsearch.usagereporting.SearchAction}
+ *  <!--@exportToFramework:else()
+ *  search action
+ *  -->
+ * {@link GenericDocument}, which contains getters for search action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchActionGenericDocument extends TakenActionGenericDocument {
+    private static final String PROPERTY_PATH_QUERY = "query";
+    private static final String PROPERTY_PATH_FETCHED_RESULT_COUNT = "fetchedResultCount";
+
+    SearchActionGenericDocument(@NonNull GenericDocument document) {
+        super(Preconditions.checkNotNull(document));
+    }
+
+    /** Returns the string value of property {@code query}. */
+    @Nullable
+    public String getQuery() {
+        return getPropertyString(PROPERTY_PATH_QUERY);
+    }
+
+    /** Returns the integer value of property {@code fetchedResultCount}. */
+    public int getFetchedResultCount() {
+        return (int) getPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT);
+    }
+
+    /** Builder for {@link SearchActionGenericDocument}. */
+    public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+        /**
+         * Creates a new {@link SearchActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace  the namespace to set for the {@link GenericDocument}.
+         * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *                   provided {@code schemaType} must be defined using
+         *                   {@link AppSearchSession#setSchemaAsync} prior
+         *                   to inserting a document of this {@code schemaType} into the
+         *                   AppSearch index using
+         *                   {@link AppSearchSession#putAsync}.
+         *                   Otherwise, the document will be rejected by
+         *                   {@link AppSearchSession#putAsync} with result code
+         *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+                    Preconditions.checkNotNull(schemaType), ActionConstants.ACTION_TYPE_SEARCH);
+        }
+
+        /**
+         * Creates a new {@link SearchActionGenericDocument.Builder} from an existing
+         * {@link GenericDocument}.
+         *
+         * @param document a generic document object.
+         *
+         * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+         *                                  not {@link ActionConstants#ACTION_TYPE_SEARCH}.
+         */
+        public Builder(@NonNull GenericDocument document) {
+            super(Preconditions.checkNotNull(document));
+
+            if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+                    != ActionConstants.ACTION_TYPE_SEARCH) {
+                throw new IllegalArgumentException(
+                        "Invalid action type for SearchActionGenericDocument");
+            }
+        }
+
+        /**
+         * Sets the string value of property {@code query} by the user-entered search input
+         * (without any operators or rewriting).
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@NonNull String query) {
+            Preconditions.checkNotNull(query);
+            setPropertyString(PROPERTY_PATH_QUERY, query);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code fetchedResultCount} by total number of results
+         * fetched from AppSearch by the client in this search action.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setFetchedResultCount(int fetchedResultCount) {
+            Preconditions.checkArgumentNonnegative(fetchedResultCount);
+            setPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT, fetchedResultCount);
+            return this;
+        }
+
+        /** Builds a {@link SearchActionGenericDocument}. */
+        @Override
+        @NonNull
+        public SearchActionGenericDocument build() {
+            return new SearchActionGenericDocument(super.build());
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java
new file mode 100644
index 0000000..72457c2
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchSessionStatsExtractor.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.localstorage.stats.ClickStats;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
+import androidx.appsearch.usagereporting.ActionConstants;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Extractor class for analyzing a list of taken action {@link GenericDocument} and creating a list
+ * of {@link SearchSessionStats}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchSessionStatsExtractor {
+    // TODO(b/319285816): make thresholds configurable.
+    /**
+     * Threshold for noise search intent detection, in millisecond. A search action will be
+     * considered as a noise (and skipped) if all of the following conditions are satisfied:
+     * <ul>
+     *     <li>The action timestamp (action document creation timestamp) difference between it and
+     *     its previous search action is below this threshold.
+     *     <li>There is no click action associated with it.
+     *     <li>Its raw query string is a prefix of the previous search action's raw query string (or
+     *     the other way around).
+     * </ul>
+     */
+    private static final long NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS = 2000L;
+
+    /**
+     * Threshold for independent search intent detection, in millisecond. If the action timestamp
+     * (action document creation timestamp) difference between the previous and the current search
+     * action exceeds this threshold, then the current search action will be considered as a
+     * completely independent search intent (i.e. belonging to a new search session), and there will
+     * be no correlation analysis between the previous and the current search action.
+     */
+    private static final long INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS =
+            10L * 60 * 1000;
+
+    /**
+     * Threshold for marking good click (compared with {@code timeStayOnResultMillis}), in
+     * millisecond. A good click means the user spent decent amount of time on the clicked result
+     * document.
+     */
+    private static final long GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS = 2000L;
+
+    /**
+     * Threshold for backspace count to become query abandonment. If the user hits backspace for at
+     * least QUERY_ABANDONMENT_BACKSPACE_COUNT times, then the query correction type will be
+     * determined as abandonment.
+     */
+    private static final int QUERY_ABANDONMENT_BACKSPACE_COUNT = 2;
+
+    /**
+     * Returns the query correction type between the previous and current search actions.
+     *
+     * @param currSearchAction the current search action {@link SearchActionGenericDocument}.
+     * @param prevSearchAction the previous search action {@link SearchActionGenericDocument}.
+     */
+    public static @SearchIntentStats.QueryCorrectionType int getQueryCorrectionType(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @Nullable SearchActionGenericDocument prevSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+
+        if (currSearchAction.getQuery() == null) {
+            // Query correction type cannot be determined if the client didn't provide the raw query
+            // string.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+        }
+        if (prevSearchAction == null) {
+            // If the previous search action is missing, then it is the first query.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY;
+        } else if (prevSearchAction.getQuery() == null) {
+            // Query correction type cannot be determined if the client didn't provide the raw query
+            // string.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+        }
+
+        // Determine the query correction type by comparing the current and previous raw query
+        // strings.
+        String prevQuery = prevSearchAction.getQuery();
+        String currQuery = currSearchAction.getQuery();
+        int commonPrefixLength = getCommonPrefixLength(prevQuery, currQuery);
+        // If the user hits backspace >= QUERY_ABANDONMENT_BACKSPACE_COUNT times, then it is query
+        // abandonment. Otherwise, it is query refinement.
+        if (commonPrefixLength <= prevQuery.length() - QUERY_ABANDONMENT_BACKSPACE_COUNT) {
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+        } else {
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT;
+        }
+    }
+
+    /**
+     * Returns a list of {@link SearchSessionStats} extracted from the given list of taken action
+     * {@link GenericDocument}.
+     *
+     * <p>A search session consists of several related search intents.
+     *
+     * <p>A search intent consists of a valid search action with 0 or more click actions. To extract
+     * search intent metrics, this function will try to group the given taken actions into several
+     * search intents, and yield a {@link SearchIntentStats} for each search intent. Finally related
+     * {@link SearchIntentStats} will be wrapped into {@link SearchSessionStats}.
+     *
+     * @param packageName The package name of the caller.
+     * @param database The database name of the caller.
+     * @param genericDocuments a list of taken actions in generic document form.
+     */
+    @NonNull
+    public List<SearchSessionStats> extract(
+            @NonNull String packageName,
+            @Nullable String database,
+            @NonNull List<GenericDocument> genericDocuments) {
+        Objects.requireNonNull(genericDocuments);
+
+        // Convert GenericDocument list to TakenActionGenericDocument list and sort them by document
+        // creation timestamp.
+        List<TakenActionGenericDocument> takenActionGenericDocuments =
+                new ArrayList<>(genericDocuments.size());
+        for (int i = 0; i < genericDocuments.size(); ++i) {
+            try {
+                takenActionGenericDocuments.add(
+                        TakenActionGenericDocument.create(genericDocuments.get(i)));
+            } catch (IllegalArgumentException e) {
+                // Skip generic documents with unknown action type.
+            }
+        }
+        Collections.sort(takenActionGenericDocuments,
+                (TakenActionGenericDocument doc1, TakenActionGenericDocument doc2) ->
+                        Long.compare(doc1.getCreationTimestampMillis(),
+                                doc2.getCreationTimestampMillis()));
+
+        List<SearchSessionStats> result = new ArrayList<>();
+        SearchSessionStats.Builder searchSessionStatsBuilder = null;
+        SearchActionGenericDocument prevSearchAction = null;
+        // Clients are expected to report search action followed by its associated click actions.
+        // For example, [searchAction1, clickAction1, searchAction2, searchAction3, clickAction2,
+        // clickAction3]:
+        // - There are 3 search actions and 3 click actions.
+        // - clickAction1 is associated with searchAction1.
+        // - There is no click action associated with searchAction2.
+        // - clickAction2 and clickAction3 are associated with searchAction3.
+        // Here we're going to break down the list into segments. Each segment starts with a search
+        // action followed by 0 or more associated click actions, and they form a single search
+        // intent. We will analyze and extract metrics from the taken actions for the search intent.
+        //
+        // If a search intent is considered independent from the previous one, then we will start a
+        // new search session analysis.
+        for (int i = 0; i < takenActionGenericDocuments.size(); ++i) {
+            if (takenActionGenericDocuments.get(i).getActionType()
+                    != ActionConstants.ACTION_TYPE_SEARCH) {
+                continue;
+            }
+
+            SearchActionGenericDocument currSearchAction =
+                    (SearchActionGenericDocument) takenActionGenericDocuments.get(i);
+            List<ClickActionGenericDocument> clickActions = new ArrayList<>();
+            // Get all click actions associated with the current search action by advancing until
+            // the next search action.
+            while (i + 1 < takenActionGenericDocuments.size()
+                    && takenActionGenericDocuments.get(i + 1).getActionType()
+                        != ActionConstants.ACTION_TYPE_SEARCH) {
+                if (takenActionGenericDocuments.get(i + 1).getActionType()
+                        == ActionConstants.ACTION_TYPE_CLICK) {
+                    clickActions.add(
+                            (ClickActionGenericDocument) takenActionGenericDocuments.get(i + 1));
+                }
+                ++i;
+            }
+
+            // Get the reference of the next search action if it exists.
+            SearchActionGenericDocument nextSearchAction = null;
+            if (i + 1 < takenActionGenericDocuments.size()
+                    && takenActionGenericDocuments.get(i + 1).getActionType()
+                        == ActionConstants.ACTION_TYPE_SEARCH) {
+                nextSearchAction =
+                        (SearchActionGenericDocument) takenActionGenericDocuments.get(i + 1);
+            }
+
+            if (prevSearchAction != null
+                    && isIndependentSearchAction(currSearchAction, prevSearchAction)) {
+                // If the current search action is independent from the previous one, then:
+                // - Build and append the previous search session stats.
+                // - Start a new search session analysis.
+                // - Ignore the previous search action when extracting stats.
+                if (searchSessionStatsBuilder != null) {
+                    result.add(searchSessionStatsBuilder.build());
+                    searchSessionStatsBuilder = null;
+                }
+                prevSearchAction = null;
+            } else if (clickActions.isEmpty()
+                    && isIntermediateSearchAction(
+                    currSearchAction, prevSearchAction, nextSearchAction)) {
+                // If the current search action is an intermediate search action with no click
+                // actions, then we consider it as a noise and skip it.
+                continue;
+            }
+
+            // Now we get a valid search intent (the current search action + a list of click actions
+            // associated with it). Extract metrics and add SearchIntentStats into this search
+            // session.
+            if (searchSessionStatsBuilder == null) {
+                searchSessionStatsBuilder =
+                        new SearchSessionStats.Builder(packageName).setDatabase(database);
+            }
+            searchSessionStatsBuilder.addSearchIntentsStats(
+                    createSearchIntentStats(
+                            packageName,
+                            database,
+                            currSearchAction,
+                            clickActions,
+                            prevSearchAction));
+            prevSearchAction = currSearchAction;
+        }
+        if (searchSessionStatsBuilder != null) {
+            result.add(searchSessionStatsBuilder.build());
+        }
+        return result;
+    }
+
+    /**
+     * Creates a {@link SearchIntentStats} object from the current search action + its associated
+     * click actions, and the previous search action (in generic document form).
+     */
+    private SearchIntentStats createSearchIntentStats(
+            @NonNull String packageName,
+            @Nullable String database,
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @NonNull List<ClickActionGenericDocument> clickActions,
+            @Nullable SearchActionGenericDocument prevSearchAction) {
+        SearchIntentStats.Builder builder = new SearchIntentStats.Builder(packageName)
+                .setDatabase(database)
+                .setTimestampMillis(currSearchAction.getCreationTimestampMillis())
+                .setCurrQuery(currSearchAction.getQuery())
+                .setNumResultsFetched(currSearchAction.getFetchedResultCount())
+                .setQueryCorrectionType(getQueryCorrectionType(currSearchAction, prevSearchAction));
+        if (prevSearchAction != null) {
+            builder.setPrevQuery(prevSearchAction.getQuery());
+        }
+        for (int i = 0; i < clickActions.size(); ++i) {
+            builder.addClicksStats(createClickStats(clickActions.get(i)));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Creates a {@link ClickStats} object from the given click action (in generic document form).
+     */
+    private ClickStats createClickStats(ClickActionGenericDocument clickAction) {
+        // A click is considered good if:
+        // - The user spent decent amount of time on the clicked document.
+        // - OR the client didn't provide timeStayOnResultMillis. In this case, the value will be 0.
+        boolean isGoodClick =
+                clickAction.getTimeStayOnResultMillis() <= 0
+                        || clickAction.getTimeStayOnResultMillis()
+                        >= GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS;
+        return new ClickStats.Builder()
+                .setTimestampMillis(clickAction.getCreationTimestampMillis())
+                .setResultRankInBlock(clickAction.getResultRankInBlock())
+                .setResultRankGlobal(clickAction.getResultRankGlobal())
+                .setTimeStayOnResultMillis(clickAction.getTimeStayOnResultMillis())
+                .setIsGoodClick(isGoodClick)
+                .build();
+    }
+
+    /**
+     * Returns if the current search action is an intermediate search action.
+     *
+     * <p>An intermediate search action is used for detecting the situation when the user adds or
+     * deletes characters from the query (e.g. "a" -> "app" -> "apple" or "apple" -> "app" -> "a")
+     * within a short period of time. More precisely, it has to satisfy all of the following
+     * conditions:
+     * <ul>
+     *     <li>There are related (non-independent) search actions before and after it.
+     *     <li>It occurs within the threshold after its previous search action.
+     *     <li>Its raw query string is a prefix of its previous search action's raw query string, or
+     *     the opposite direction.
+     * </ul>
+     */
+    private static boolean isIntermediateSearchAction(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @Nullable SearchActionGenericDocument prevSearchAction,
+            @Nullable SearchActionGenericDocument nextSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+
+        if (prevSearchAction == null || nextSearchAction == null) {
+            return false;
+        }
+
+        // Whether the next search action is independent from the current search action. If true,
+        // then the current search action will not be considered as an intermediate search action
+        // since it is the last search action of the search session.
+        boolean isNextSearchActionIndependent =
+                isIndependentSearchAction(nextSearchAction, currSearchAction);
+
+        // Whether the current search action occurs within the threshold after the previous search
+        // action.
+        boolean occursWithinTimeThreshold =
+                currSearchAction.getCreationTimestampMillis()
+                        - prevSearchAction.getCreationTimestampMillis()
+                        <= NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+
+        // Whether the previous search action's raw query string is a prefix of the current search
+        // action's, or the opposite direction (e.g. "app" -> "apple" and "apple" -> "app").
+        String prevQuery = prevSearchAction.getQuery();
+        String currQuery = currSearchAction.getQuery();
+        boolean isPrefix = prevQuery != null && currQuery != null
+                && (currQuery.startsWith(prevQuery) || prevQuery.startsWith(currQuery));
+
+        return !isNextSearchActionIndependent && occursWithinTimeThreshold && isPrefix;
+    }
+
+    /**
+     * Returns if the current search action is independent from the previous search action.
+     *
+     * <p>If the current search action occurs later than the threshold after the previous search
+     * action, then they are considered independent.
+     */
+    private static boolean isIndependentSearchAction(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @NonNull SearchActionGenericDocument prevSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+        Objects.requireNonNull(prevSearchAction);
+
+        long searchTimeDiffMillis = currSearchAction.getCreationTimestampMillis()
+                - prevSearchAction.getCreationTimestampMillis();
+        return searchTimeDiffMillis > INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+    }
+
+    /** Returns the common prefix length of the given 2 strings. */
+    private static int getCommonPrefixLength(@NonNull String s1, @NonNull String s2) {
+        Objects.requireNonNull(s1);
+        Objects.requireNonNull(s2);
+
+        int minLength = Math.min(s1.length(), s2.length());
+        for (int i = 0; i < minLength; ++i) {
+            if (s1.charAt(i) != s2.charAt(i)) {
+                return i;
+            }
+        }
+        return minLength;
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java
new file mode 100644
index 0000000..1af156d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Abstract wrapper class for {@link GenericDocument} of all types of taken actions, which contains
+ * common getters and constants.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class TakenActionGenericDocument extends GenericDocument {
+    protected static final String PROPERTY_PATH_ACTION_TYPE = "actionType";
+
+    /**
+     * Static factory method to create a concrete object of {@link TakenActionGenericDocument} child
+     * type, according to the given {@link GenericDocument}'s action type.
+     *
+     * @param document a generic document object.
+     *
+     * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+     *                                  invalid.
+     */
+    @NonNull
+    public static TakenActionGenericDocument create(@NonNull GenericDocument document)
+            throws IllegalArgumentException {
+        Preconditions.checkNotNull(document);
+        int actionType = (int) document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+        switch (actionType) {
+            case ActionConstants.ACTION_TYPE_SEARCH:
+                return new SearchActionGenericDocument.Builder(document).build();
+            case ActionConstants.ACTION_TYPE_CLICK:
+                return new ClickActionGenericDocument.Builder(document).build();
+            default:
+                throw new IllegalArgumentException(
+                        "Cannot create taken action generic document with unknown action type");
+        }
+    }
+
+    protected TakenActionGenericDocument(@NonNull GenericDocument document) {
+        super(Preconditions.checkNotNull(document));
+    }
+
+    /** Returns the (enum) integer value of property {@code actionType}. */
+    public int getActionType() {
+        return (int) getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+    }
+
+    /** Abstract builder for {@link TakenActionGenericDocument}. */
+    abstract static class Builder<T extends Builder<T>> extends GenericDocument.Builder<T> {
+        /**
+         * Creates a new {@link TakenActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace  the namespace to set for the {@link GenericDocument}.
+         * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *                   provided {@code schemaType} must be defined using
+         *                   {@link AppSearchSession#setSchemaAsync} prior
+         *                   to inserting a document of this {@code schemaType} into the
+         *                   AppSearch index using
+         *                   {@link AppSearchSession#putAsync}.
+         *                   Otherwise, the document will be rejected by
+         *                   {@link AppSearchSession#putAsync} with result code
+         *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
+         * @param actionType the action type of the taken action. See definitions in
+         *                   {@link ActionConstants}.
+         */
+        Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType,
+                int actionType) {
+            super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id),
+                    Preconditions.checkNotNull(schemaType));
+
+            setPropertyLong(PROPERTY_PATH_ACTION_TYPE, actionType);
+        }
+
+        /**
+         * Creates a new {@link TakenActionGenericDocument.Builder} from an existing
+         * {@link GenericDocument}.
+         */
+        Builder(@NonNull GenericDocument document) {
+            super(Preconditions.checkNotNull(document));
+        }
+    }
+}
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
index a22863a..4deb5c3 100644
--- 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
@@ -44,10 +44,19 @@
         return mCallingPackageName;
     }
 
+    /** Returns whether the caller should have default access to data in its own package. */
+    public boolean doesCallerHaveSelfAccess() {
+        return true;
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
-        if (this == o) return true;
-        if (!(o instanceof CallerAccess)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof CallerAccess)) {
+            return false;
+        }
         CallerAccess that = (CallerAccess) o;
         return mCallingPackageName.equals(that.mCallingPackageName);
     }
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
index 9db52a5..13f908d 100644
--- 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
@@ -38,4 +38,11 @@
             @NonNull String packageName,
             @NonNull String prefixedSchema,
             @NonNull VisibilityStore visibilityStore);
+
+    /**
+     * Checks whether the given package has access to system-surfaceable schemas.
+     *
+     * @param callerPackageName Package name of the caller.
+     */
+    boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName);
 }
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
index a45cf75..7bcea68 100644
--- 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
@@ -18,6 +18,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.PackageIdentifier;
@@ -143,6 +144,7 @@
         }
 
         /** Sets whether this schema has opted out of platform surfacing. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
             return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
@@ -150,6 +152,7 @@
         }
 
         /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
             Preconditions.checkNotNull(packageIdentifiers);
@@ -158,6 +161,7 @@
         }
 
         /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
             Preconditions.checkNotNull(packageIdentifier);
@@ -167,6 +171,7 @@
 
         /** Add a set of Android role that has access to the schema this
          * {@link VisibilityDocumentV1} represents. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setVisibleToRoles(@NonNull Set<Integer> visibleToRoles) {
             Preconditions.checkNotNull(visibleToRoles);
@@ -176,6 +181,7 @@
 
         /** Add a set of Android role that has access to the schema this
          * {@link VisibilityDocumentV1} represents. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
             Preconditions.checkNotNull(visibleToPermissions);
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 e3d5916..c221673 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
@@ -27,11 +27,15 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
-import androidx.appsearch.app.VisibilityDocument;
-import androidx.appsearch.app.VisibilityPermissionDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.checker.initialization.qual.UnderInitialization;
+import androidx.appsearch.checker.initialization.qual.UnknownInitialization;
+import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.util.LogUtil;
 import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
 
@@ -47,10 +51,11 @@
  * 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>The VisibilityStore creates a {@link InternalVisibilityConfig} for each schema. This config
+ * holds the visibility settings that apply to that schema. The VisibilityStore also creates a
+ * schema and documents for these {@link InternalVisibilityConfig} 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.
@@ -67,12 +72,13 @@
     public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg";
 
     public static final String VISIBILITY_DATABASE_NAME = "VS#Db";
+    public static final String ANDROID_V_OVERLAY_DATABASE_NAME = "VS#AndroidVDb";
 
     /**
-     * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each
+     * Map of PrefixedSchemaType to InternalVisibilityConfig stores visibility information for each
      * schema type.
      */
-    private final Map<String, VisibilityDocument> mVisibilityDocumentMap = new ArrayMap<>();
+    private final Map<String, InternalVisibilityConfig> mVisibilityConfigMap = new ArrayMap<>();
 
     private final AppSearchImpl mAppSearchImpl;
 
@@ -86,7 +92,7 @@
                 new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
         List<VisibilityDocumentV1> visibilityDocumentsV1s = null;
         switch (getSchemaResponse.getVersion()) {
-            case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:
+            case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_PACKAGE:
                 // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using
                 //  GenericDocument.
                 List<GenericDocument> visibilityDocumentsV0s =
@@ -95,7 +101,7 @@
                 visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0
                         .toVisibilityDocumentV1(visibilityDocumentsV0s);
                 // fall through
-            case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA:
+            case VisibilityToDocumentConverter.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.
@@ -106,37 +112,12 @@
                 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, see comment below.
-                    InternalSetSchemaResponse internalSetSchemaResponse = 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);
-                    if (!internalSetSchemaResponse.isSuccess()) {
-                        // 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.
-                        throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
-                                "Fail to set the latest visibility schema to AppSearch. "
-                                        + "You may need to update the visibility schema version "
-                                        + "number.");
-                    }
-                }
+            case VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST:
+                verifyOrSetLatestVisibilitySchema(getSchemaResponse);
+                // Check the version for visibility overlay database.
+                migrateVisibilityOverlayDatabase();
+                // Now we have the latest schema, load visibility config map.
+                loadVisibilityConfigMap();
                 break;
             default:
                 // We must did something wrong.
@@ -146,30 +127,64 @@
     }
 
     /**
-     * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous
-     * {@link VisibilityDocument}s with same prefixed schema type will be overwritten.
+     * Sets visibility settings for the given {@link InternalVisibilityConfig}s. Any previous
+     * {@link InternalVisibilityConfig}s with same prefixed schema type will be overwritten.
      *
-     * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which
-     *                                    contains schema type's visibility information.
+     * @param prefixedVisibilityConfigs List of prefixed {@link InternalVisibilityConfig}s which
+     *                                  contains schema type's visibility information.
      * @throws AppSearchException on AppSearchImpl error.
      */
-    public void setVisibility(@NonNull List<VisibilityDocument> prefixedVisibilityDocuments)
+    public void setVisibility(@NonNull List<InternalVisibilityConfig> prefixedVisibilityConfigs)
             throws AppSearchException {
-        Preconditions.checkNotNull(prefixedVisibilityDocuments);
+        Preconditions.checkNotNull(prefixedVisibilityConfigs);
         // 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);
+        for (int i = 0; i < prefixedVisibilityConfigs.size(); i++) {
+            // put VisibilityConfig to AppSearchImpl and mVisibilityConfigMap. If there is a
+            // VisibilityConfig with same prefixed schema exists, it will be replaced by new
+            // VisibilityConfig in both AppSearch and memory look up map.
+            InternalVisibilityConfig prefixedVisibilityConfig = prefixedVisibilityConfigs.get(i);
+            InternalVisibilityConfig oldVisibilityConfig =
+                    mVisibilityConfigMap.get(prefixedVisibilityConfig.getSchemaType());
             mAppSearchImpl.putDocument(
                     VISIBILITY_PACKAGE_NAME,
                     VISIBILITY_DATABASE_NAME,
-                    prefixedVisibilityDocument.toGenericDocument(),
+                    VisibilityToDocumentConverter.createVisibilityDocument(
+                            prefixedVisibilityConfig),
                     /*sendChangeNotifications=*/ false,
                     /*logger=*/ null);
-            mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(),
-                    prefixedVisibilityDocument);
+
+            // Put the android V visibility overlay document to AppSearchImpl.
+            GenericDocument androidVOverlay =
+                    VisibilityToDocumentConverter.createAndroidVOverlay(prefixedVisibilityConfig);
+            if (androidVOverlay != null) {
+                mAppSearchImpl.putDocument(
+                        VISIBILITY_PACKAGE_NAME,
+                        ANDROID_V_OVERLAY_DATABASE_NAME,
+                        androidVOverlay,
+                        /*sendChangeNotifications=*/ false,
+                        /*logger=*/ null);
+            } else if (isConfigContainsAndroidVOverlay(oldVisibilityConfig)) {
+                // We need to make sure to remove the VisibilityOverlay on disk as the current
+                // VisibilityConfig does not have a VisibilityOverlay.
+                // For performance improvement, we should only make the remove call if the old
+                // VisibilityConfig contains the overlay settings.
+                try {
+                    mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,
+                            ANDROID_V_OVERLAY_DATABASE_NAME,
+                            VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                            prefixedVisibilityConfig.getSchemaType(),
+                            /*removeStatsBuilder=*/null);
+                } catch (AppSearchException e) {
+                    // If it already doesn't exist, that is fine
+                    if (e.getResultCode() != RESULT_NOT_FOUND) {
+                        throw e;
+                    }
+                }
+            }
+
+            // Put the VisibilityConfig to memory look up map.
+            mVisibilityConfigMap.put(prefixedVisibilityConfig.getSchemaType(),
+                    prefixedVisibilityConfig);
         }
         // Now that the visibility document has been written. Persist the newly written data.
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
@@ -182,12 +197,13 @@
     public void removeVisibility(@NonNull Set<String> prefixedSchemaTypes)
             throws AppSearchException {
         for (String prefixedSchemaType : prefixedSchemaTypes) {
-            if (mVisibilityDocumentMap.remove(prefixedSchemaType) != null) {
+            if (mVisibilityConfigMap.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,
+                            VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                            prefixedSchemaType,
                             /*removeStatsBuilder=*/null);
                 } catch (AppSearchException e) {
                     if (e.getResultCode() == RESULT_NOT_FOUND) {
@@ -195,25 +211,45 @@
                         // to be fine if we cannot find it.
                         Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType
                                 + " to remove.");
-                        return;
+                    } else {
+                        throw e;
                     }
-                    throw e;
+                }
+
+                try {
+                    mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME,
+                            ANDROID_V_OVERLAY_DATABASE_NAME,
+                            VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                            prefixedSchemaType,
+                            /*removeStatsBuilder=*/null);
+                } catch (AppSearchException e) {
+                    if (e.getResultCode() == RESULT_NOT_FOUND) {
+                        // It's possible no overlay was set, so this this is fine.
+                        if (LogUtil.DEBUG) {
+                            Log.d(TAG, "Cannot find Android V overlay document for "
+                                    + prefixedSchemaType + " to remove.");
+                        }
+                    } else {
+                        throw e;
+                    }
                 }
             }
         }
     }
 
-    /** Gets the {@link VisibilityDocument} for the given prefixed schema type.     */
+    /** Gets the {@link InternalVisibilityConfig} for the given prefixed schema type.     */
     @Nullable
-    public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) {
-        return mVisibilityDocumentMap.get(prefixedSchemaType);
+    public InternalVisibilityConfig getVisibility(@NonNull String prefixedSchemaType) {
+        return mVisibilityConfigMap.get(prefixedSchemaType);
     }
 
     /**
-     * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into
-     * {@link #mVisibilityDocumentMap}.
+     * Loads all stored latest {@link InternalVisibilityConfig} from Icing, and put them into
+     * {@link #mVisibilityConfigMap}.
      */
-    private void loadVisibilityDocumentMap() throws AppSearchException {
+    @RequiresNonNull("mAppSearchImpl")
+    private void loadVisibilityConfigMap(@UnderInitialization VisibilityStore this)
+            throws AppSearchException {
         // Populate visibility settings set
         List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes();
         for (int i = 0; i < cachedSchemaTypes.size(); i++) {
@@ -223,16 +259,16 @@
                 continue; // Our own package. Skip.
             }
 
-            VisibilityDocument visibilityDocument;
+            GenericDocument visibilityDocument;
+            GenericDocument visibilityAndroidVOverlay = null;
             try {
                 // Note: We use the other clients' prefixed schema type as ids
-                visibilityDocument = new VisibilityDocument.Builder(
-                        mAppSearchImpl.getDocument(
-                                VISIBILITY_PACKAGE_NAME,
-                                VISIBILITY_DATABASE_NAME,
-                                VisibilityDocument.NAMESPACE,
-                                /*id=*/ prefixedSchemaType,
-                                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+                visibilityDocument = mAppSearchImpl.getDocument(
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_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
@@ -242,25 +278,48 @@
                 // Otherwise, this is some other error we should pass up.
                 throw e;
             }
-            mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument);
+
+            try {
+                visibilityAndroidVOverlay = mAppSearchImpl.getDocument(
+                        VISIBILITY_PACKAGE_NAME,
+                        ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ prefixedSchemaType,
+                        /*typePropertyPaths=*/ Collections.emptyMap());
+            } catch (AppSearchException e) {
+                if (e.getResultCode() != RESULT_NOT_FOUND) {
+                    // This is some other error we should pass up.
+                    throw e;
+                }
+                // Otherwise we continue inserting into visibility document map as the overlay
+                // map can be null
+            }
+
+            mVisibilityConfigMap.put(
+                    prefixedSchemaType,
+                    VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                            visibilityDocument, visibilityAndroidVOverlay));
         }
     }
 
     /**
-     * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch.
+     * Set the latest version of {@link InternalVisibilityConfig} and its schema to AppSearch.
      */
-    private void setLatestSchemaAndDocuments(@NonNull List<VisibilityDocument> migratedDocuments)
+    @RequiresNonNull("mAppSearchImpl")
+    private void setLatestSchemaAndDocuments(
+            @UnderInitialization VisibilityStore this,
+            @NonNull List<InternalVisibilityConfig> migratedDocuments)
             throws AppSearchException {
         // The latest schema type doesn't exist yet. Add it. Set forceOverride true to
         // delete old schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 VISIBILITY_PACKAGE_NAME,
                 VISIBILITY_DATABASE_NAME,
-                Arrays.asList(VisibilityDocument.SCHEMA,
-                        VisibilityPermissionDocument.SCHEMA),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                        VisibilityPermissionConfig.SCHEMA),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
-                /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
                 /*setSchemaStatsBuilder=*/ null);
         if (!internalSetSchemaResponse.isSuccess()) {
             // Impossible case, we just set forceOverride to be true, we should never
@@ -268,15 +327,185 @@
             throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
                     internalSetSchemaResponse.getErrorMessage());
         }
+        InternalSetSchemaResponse internalSetAndroidVOverlaySchemaResponse =
+                mAppSearchImpl.setSchema(
+                        VISIBILITY_PACKAGE_NAME,
+                        ANDROID_V_OVERLAY_DATABASE_NAME,
+                        Collections.singletonList(
+                                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+                        /*visibilityConfigs=*/ Collections.emptyList(),
+                        /*forceOverride=*/ true,
+                        /*version=*/ VisibilityToDocumentConverter
+                                .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+                        /*setSchemaStatsBuilder=*/ null);
+        if (!internalSetAndroidVOverlaySchemaResponse.isSuccess()) {
+            // Impossible case, we just set forceOverride to be true, we should never
+            // fail in incompatible changes.
+            throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                    internalSetAndroidVOverlaySchemaResponse.getErrorMessage());
+        }
         for (int i = 0; i < migratedDocuments.size(); i++) {
-            VisibilityDocument migratedDocument = migratedDocuments.get(i);
-            mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument);
+            InternalVisibilityConfig migratedConfig = migratedDocuments.get(i);
+            mVisibilityConfigMap.put(migratedConfig.getSchemaType(), migratedConfig);
             mAppSearchImpl.putDocument(
                     VISIBILITY_PACKAGE_NAME,
                     VISIBILITY_DATABASE_NAME,
-                    migratedDocument.toGenericDocument(),
+                    VisibilityToDocumentConverter.createVisibilityDocument(migratedConfig),
                     /*sendChangeNotifications=*/ false,
                     /*logger=*/ null);
         }
     }
+
+    /**
+     * Check and migrate visibility schemas in {@link #ANDROID_V_OVERLAY_DATABASE_NAME} to
+     * {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST}.
+     */
+    @RequiresNonNull("mAppSearchImpl")
+    private void migrateVisibilityOverlayDatabase(@UnderInitialization VisibilityStore this)
+            throws AppSearchException {
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME));
+        switch (getSchemaResponse.getVersion()) {
+            case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG:
+                // Force override to next version. This version hasn't released to any public
+                // version. There shouldn't have any public device in this state, so we don't
+                // actually need to migrate any document.
+                InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                        VISIBILITY_PACKAGE_NAME,
+                        ANDROID_V_OVERLAY_DATABASE_NAME,
+                        Collections.singletonList(
+                                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+                        /*visibilityConfigs=*/ Collections.emptyList(),
+                        /*forceOverride=*/ true,  // force update to nest version.
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+                        /*setSchemaStatsBuilder=*/ null);
+                if (!internalSetSchemaResponse.isSuccess()) {
+                    // Impossible case, we just set forceOverride to be true, we should never
+                    // fail in incompatible changes.
+                    throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                            internalSetSchemaResponse.getErrorMessage());
+                }
+                break;
+            case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO:
+                verifyOrSetLatestVisibilityOverlaySchema(getSchemaResponse);
+                break;
+            default:
+                // We must did something wrong.
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Found unsupported visibility version: " + getSchemaResponse.getVersion());
+        }
+    }
+
+    /**
+     * Verify the existing visibility schema, set the latest visibilility schema if it's missing.
+     */
+    @RequiresNonNull("mAppSearchImpl")
+    private void verifyOrSetLatestVisibilitySchema(
+            @UnderInitialization VisibilityStore this, @NonNull GetSchemaResponse getSchemaResponse)
+            throws AppSearchException {
+        // We cannot change the schema version past 2 as detecting version "3" would hit the
+        // default block and throw an AppSearchException. This is why we added
+        // VisibilityOverlay.
+
+        // Check Visibility schema first.
+        Set<AppSearchSchema> existingVisibilitySchema = getSchemaResponse.getSchemas();
+        // Force to override visibility schema if it contains DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA.
+        // The DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA was added to VISIBILITY_DATABASE_NAME and
+        // removed to ANDROID_V_OVERLAY_DATABASE_NAME. We need to force update the schema to
+        // migrate devices that have already store public acl schema.
+        // TODO(b/321326441) remove this method when we no longer to migrate devices in this state.
+        if (existingVisibilitySchema.contains(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)
+                && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA)
+                && existingVisibilitySchema.contains(
+                VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA)) {
+            InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                    VISIBILITY_PACKAGE_NAME,
+                    VISIBILITY_DATABASE_NAME,
+                    Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                            VisibilityPermissionConfig.SCHEMA),
+                    /*visibilityConfigs=*/ Collections.emptyList(),
+                    /*forceOverride=*/ true,
+                    /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                    /*setSchemaStatsBuilder=*/ null);
+            if (!internalSetSchemaResponse.isSuccess()) {
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Fail to force override deprecated visibility schema with public acl.");
+            }
+        } else if (!(existingVisibilitySchema.contains(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA)
+                && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA))) {
+            // We must have a broken schema. Reset it to the latest version.
+            // Do NOT set forceOverride to be true here, see comment below.
+            InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                    VISIBILITY_PACKAGE_NAME,
+                    VISIBILITY_DATABASE_NAME,
+                    Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                            VisibilityPermissionConfig.SCHEMA),
+                    /*visibilityConfigs=*/ Collections.emptyList(),
+                    /*forceOverride=*/ false,
+                    /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                    /*setSchemaStatsBuilder=*/ null);
+            if (!internalSetSchemaResponse.isSuccess()) {
+                // 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.
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Fail to set the latest visibility schema to AppSearch. "
+                                + "You may need to update the visibility schema version "
+                                + "number.");
+            }
+        }
+    }
+
+    /**
+     * Verify the existing visibility overlay schema, set the latest overlay schema if it's missing.
+     */
+    @RequiresNonNull("mAppSearchImpl")
+    private void verifyOrSetLatestVisibilityOverlaySchema(
+            @UnknownInitialization VisibilityStore this,
+            @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse)
+            throws AppSearchException {
+        // Check Android V overlay schema.
+        Set<AppSearchSchema> existingAndroidVOverlaySchema =
+                getAndroidVOverlaySchemaResponse.getSchemas();
+        if (!existingAndroidVOverlaySchema.contains(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA)) {
+            // We must have a broken schema. Reset it to the latest version.
+            // Do NOT set forceOverride to be true here, see comment below.
+            InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                    VISIBILITY_PACKAGE_NAME,
+                    ANDROID_V_OVERLAY_DATABASE_NAME,
+                    Collections.singletonList(
+                            VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
+                    /*visibilityConfigs=*/ Collections.emptyList(),
+                    /*forceOverride=*/ false,
+                    VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+                    /*setSchemaStatsBuilder=*/ null);
+            if (!internalSetSchemaResponse.isSuccess()) {
+                // If you hit problem here it means you made a incompatible change in
+                // Visibility Schema. You should create new overlay schema
+                throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                        "Fail to set the overlay visibility schema to AppSearch. "
+                                + "You may need to create new overlay schema.");
+            }
+        }
+    }
+
+    /**
+     * Whether the given {@link InternalVisibilityConfig} contains Android V overlay settings.
+     *
+     * <p> Android V overlay {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA}
+     * contains public acl and visible to config.
+     */
+    private static boolean isConfigContainsAndroidVOverlay(
+            @Nullable InternalVisibilityConfig config) {
+        return config != null
+                && (config.getVisibilityConfig().getPubliclyVisibleTargetPackage() != null
+                || !config.getVisibleToConfigs().isEmpty());
+    }
 }
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
index 4fb4de9..ca255bf 100644
--- 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
@@ -24,7 +24,6 @@
 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;
@@ -153,7 +152,7 @@
                     deprecatedDocuments.add(appSearchImpl.getDocument(
                             VisibilityStore.VISIBILITY_PACKAGE_NAME,
                             VisibilityStore.VISIBILITY_DATABASE_NAME,
-                            VisibilityDocument.NAMESPACE,
+                            VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                             getDeprecatedVisibilityDocumentId(packageName, databaseName),
                             /*typePropertyPaths=*/ Collections.emptyMap()));
                 } catch (AppSearchException e) {
@@ -206,15 +205,15 @@
                 for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
                     String prefixedSchemaType = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyString(
-                            DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
+                                    DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
                     VisibilityDocumentV1.Builder visibilityBuilder =
                             getOrCreateBuilder(documentBuilderMap, prefixedSchemaType);
                     String packageName = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyString(
-                                DEPRECATED_PACKAGE_NAME_PROPERTY));
+                                    DEPRECATED_PACKAGE_NAME_PROPERTY));
                     byte[] sha256Cert = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyBytes(
-                                DEPRECATED_SHA_256_CERT_PROPERTY));
+                                    DEPRECATED_SHA_256_CERT_PROPERTY));
                     visibilityBuilder.addVisibleToPackage(
                             new PackageIdentifier(packageName, sha256Cert));
                 }
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
index 5a082fc..185c19c 100644
--- 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
@@ -20,9 +20,9 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.InternalVisibilityConfig;
 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;
@@ -67,7 +67,7 @@
                 visibilityDocumentV1s.add(new VisibilityDocumentV1(appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         allPrefixedSchemaTypes.get(i),
                         /*typePropertyPaths=*/ Collections.emptyMap())));
             } catch (AppSearchException e) {
@@ -91,13 +91,13 @@
      * @param visibilityDocumentV1s          The deprecated Visibility Document we found.
      */
     @NonNull
-    static List<VisibilityDocument> toVisibilityDocumentsV2(
+    static List<InternalVisibilityConfig> toVisibilityDocumentsV2(
             @NonNull List<VisibilityDocumentV1> visibilityDocumentV1s) {
-        List<VisibilityDocument> latestVisibilityDocuments =
+        List<InternalVisibilityConfig> 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<Set<Integer>> visibleToPermissionSets = new ArraySet<>();
             Set<Integer> deprecatedVisibleToRoles = visibilityDocumentV1.getVisibleToRoles();
             if (deprecatedVisibleToRoles != null) {
                 for (int deprecatedVisibleToRole : deprecatedVisibleToRoles) {
@@ -111,29 +111,28 @@
                                     .READ_ASSISTANT_APP_SEARCH_DATA);
                             break;
                     }
-                    visibleToPermissions.add(visibleToPermission);
+                    visibleToPermissionSets.add(visibleToPermission);
                 }
             }
             Set<Integer> deprecatedVisibleToPermissions =
                     visibilityDocumentV1.getVisibleToPermissions();
             if (deprecatedVisibleToPermissions != null) {
-                visibleToPermissions.add(deprecatedVisibleToPermissions);
+                visibleToPermissionSets.add(deprecatedVisibleToPermissions);
             }
 
-            Set<PackageIdentifier> packageIdentifiers = new ArraySet<>();
+            InternalVisibilityConfig.Builder latestVisibilityDocumentBuilder =
+                    new InternalVisibilityConfig.Builder(visibilityDocumentV1.getId())
+                            .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem());
             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]));
+                    latestVisibilityDocumentBuilder.addVisibleToPackage(
+                            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);
+            for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+                latestVisibilityDocumentBuilder.addVisibleToPermissions(visibleToPermissions);
             }
             latestVisibilityDocuments.add(latestVisibilityDocumentBuilder.build());
         }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
new file mode 100644
index 0000000..c7c5606
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 android.util.Log;
+
+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.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.collection.ArraySet;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.android.icing.protobuf.InvalidProtocolBufferException;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityToDocumentConverter {
+    private static final String TAG = "AppSearchVisibilityToDo";
+
+    /**
+     * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
+     */
+    public static final String VISIBILITY_DOCUMENT_SCHEMA_TYPE = "VisibilityType";
+    /** Namespace of documents that contain visibility settings */
+    public static final String VISIBILITY_DOCUMENT_NAMESPACE = "";
+
+    /**
+     * The Schema type for the Android V visibility setting overlay documents, that allow for
+     * additional visibility settings.
+     */
+    public static final String ANDROID_V_OVERLAY_SCHEMA_TYPE = "AndroidVOverlayType";
+    /** Namespace of documents that contain Android V visibility setting overlay documents */
+    public static final String ANDROID_V_OVERLAY_NAMESPACE = "androidVOverlay";
+    /** Property that holds the serialized {@link AndroidVOverlayProto}. */
+    public static final String VISIBILITY_PROTO_SERIALIZE_PROPERTY =
+            "visibilityProtoSerializeProperty";
+
+    /**
+     * 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 VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    private static final String VISIBLE_TO_PACKAGE_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 VisibilityConfig contains all visibility information for
+    // whole package.
+    public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+    // One VisibilityConfig contains visibility information for a single schema.
+    public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+    // One VisibilityConfig contains visibility information for a single schema. The permission
+    // visibility information is stored in a document property VisibilityPermissionConfig of the
+    // outer doc.
+    public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+    public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+    // The initial schema version, the overlay schema contains public acl and visible to config
+    // properties.
+    public static final int OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG = 0;
+
+    // The overlay schema only contains a proto property contains all visibility setting.
+    public static final int OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO = 1;
+
+    // The version number of schema saved in Android V overlay database.
+    public static final int ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST =
+            OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO;
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * <p>NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}.
+     */
+    public static final AppSearchSchema VISIBILITY_DOCUMENT_SCHEMA =
+            new AppSearchSchema.Builder(VISIBILITY_DOCUMENT_SCHEMA_TYPE)
+                    .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                            NOT_DISPLAYED_BY_SYSTEM_PROPERTY)
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .build())
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                            VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY)
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                            .build())
+                    .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                            VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY)
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                            .build())
+                    .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                            PERMISSION_PROPERTY, VisibilityPermissionConfig.SCHEMA_TYPE)
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                            .build())
+                    .build();
+
+    /**  Schema for the VisibilityStore's Android V visibility setting overlay. */
+    public static final AppSearchSchema ANDROID_V_OVERLAY_SCHEMA =
+            new AppSearchSchema.Builder(ANDROID_V_OVERLAY_SCHEMA_TYPE)
+                    .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                            VISIBILITY_PROTO_SERIALIZE_PROPERTY)
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .build())
+                    .build();
+    /**
+     * The Deprecated schemas and properties that we need to remove from visibility database.
+     * TODO(b/321326441) remove this method when we no longer to migrate devices in this state.
+     */
+    static final AppSearchSchema DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA =
+            new AppSearchSchema.Builder("PublicAclOverlayType")
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                            "publiclyVisibleTargetPackage")
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .build())
+                    .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                            "publiclyVisibleTargetPackageSha256Cert")
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .build())
+                    .build();
+
+    /**
+     * Constructs a {@link InternalVisibilityConfig} from two {@link GenericDocument}s.
+     *
+     * <p>This constructor is still needed until we don't treat Visibility related documents as
+     * {@link GenericDocument}s internally.
+     *
+     * @param visibilityDocument       a {@link GenericDocument} holding visibility properties
+     *                                 in {@link #VISIBILITY_DOCUMENT_SCHEMA}
+     * @param androidVOverlayDocument  a {@link GenericDocument} holding visibility properties
+     *                                 in {@link #ANDROID_V_OVERLAY_SCHEMA}
+     */
+    @NonNull
+    public static InternalVisibilityConfig createInternalVisibilityConfig(
+            @NonNull GenericDocument visibilityDocument,
+            @Nullable GenericDocument androidVOverlayDocument) {
+        Objects.requireNonNull(visibilityDocument);
+
+        // Parse visibility proto if required
+        AndroidVOverlayProto androidVOverlayProto = null;
+        if (androidVOverlayDocument != null) {
+            try {
+                byte[] androidVOverlayProtoBytes = androidVOverlayDocument.getPropertyBytes(
+                        VISIBILITY_PROTO_SERIALIZE_PROPERTY);
+                if (androidVOverlayProtoBytes != null) {
+                    androidVOverlayProto = AndroidVOverlayProto.parseFrom(
+                            androidVOverlayProtoBytes);
+                }
+            } catch (InvalidProtocolBufferException e) {
+                Log.e(TAG, "Get an invalid android V visibility overlay proto.", e);
+            }
+        }
+
+        // Handle all visibility settings other than visibleToConfigs
+        SchemaVisibilityConfig schemaVisibilityConfig = createVisibilityConfig(
+                visibilityDocument, androidVOverlayProto);
+
+        // Handle visibleToConfigs
+        String schemaType = visibilityDocument.getId();
+        InternalVisibilityConfig.Builder builder = new InternalVisibilityConfig.Builder(schemaType)
+                .setNotDisplayedBySystem(visibilityDocument
+                        .getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY))
+                .setVisibilityConfig(schemaVisibilityConfig);
+        if (androidVOverlayProto != null) {
+            List<VisibilityConfigProto> visibleToConfigProtoList =
+                    androidVOverlayProto.getVisibleToConfigsList();
+            for (int i = 0; i < visibleToConfigProtoList.size(); i++) {
+                SchemaVisibilityConfig visibleToConfig =
+                        convertVisibilityConfigFromProto(visibleToConfigProtoList.get(i));
+                builder.addVisibleToConfig(visibleToConfig);
+            }
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Constructs a {@link SchemaVisibilityConfig} from a {@link GenericDocument} containing legacy
+     * visibility settings, and an {@link AndroidVOverlayProto} containing extended visibility
+     * settings.
+     *
+     * <p>This constructor is still needed until we don't treat Visibility related documents as
+     * {@link GenericDocument}s internally.
+     *
+     * @param visibilityDocument   a {@link GenericDocument} holding all visibility properties
+     *                             other than publiclyVisibleTargetPackage.
+     * @param androidVOverlayProto the proto containing post-V visibility settings
+     */
+    @NonNull
+    private static SchemaVisibilityConfig createVisibilityConfig(
+            @NonNull GenericDocument visibilityDocument,
+            @Nullable AndroidVOverlayProto androidVOverlayProto) {
+        Objects.requireNonNull(visibilityDocument);
+
+        // Pre-V visibility settings come from visibilityDocument
+        SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();
+
+        String[] visibleToPackageNames =
+                visibilityDocument.getPropertyStringArray(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY);
+        byte[][] visibleToPackageShaCerts =
+                visibilityDocument.getPropertyBytesArray(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY);
+        if (visibleToPackageNames != null && visibleToPackageShaCerts != null) {
+            for (int i = 0; i < visibleToPackageNames.length; i++) {
+                builder.addAllowedPackage(
+                        new PackageIdentifier(
+                                visibleToPackageNames[i], visibleToPackageShaCerts[i]));
+            }
+        }
+
+        GenericDocument[] visibleToPermissionDocs =
+                visibilityDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);
+        if (visibleToPermissionDocs != null) {
+            for (int i = 0; i < visibleToPermissionDocs.length; ++i) {
+                long[] visibleToPermissionLongs = visibleToPermissionDocs[i].getPropertyLongArray(
+                        VisibilityPermissionConfig.ALL_REQUIRED_PERMISSIONS_PROPERTY);
+                if (visibleToPermissionLongs != null) {
+                    Set<Integer> allRequiredPermissions =
+                            new ArraySet<>(visibleToPermissionLongs.length);
+                    for (int j = 0; j < visibleToPermissionLongs.length; j++) {
+                        allRequiredPermissions.add((int) visibleToPermissionLongs[j]);
+                    }
+                    builder.addRequiredPermissions(allRequiredPermissions);
+                }
+            }
+        }
+
+        // Post-V visibility settings come from androidVOverlayProto
+        if (androidVOverlayProto != null) {
+            SchemaVisibilityConfig androidVOverlayConfig =
+                    convertVisibilityConfigFromProto(
+                            androidVOverlayProto.getVisibilityConfig());
+            builder.setPubliclyVisibleTargetPackage(
+                    androidVOverlayConfig.getPubliclyVisibleTargetPackage());
+        }
+
+        return builder.build();
+    }
+
+    @NonNull
+    private static SchemaVisibilityConfig convertVisibilityConfigFromProto(
+            @NonNull VisibilityConfigProto proto) {
+        SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder();
+
+        List<PackageIdentifierProto> visibleToPackageProtoList = proto.getVisibleToPackagesList();
+        for (int i = 0; i < visibleToPackageProtoList.size(); i++) {
+            PackageIdentifierProto visibleToPackage = proto.getVisibleToPackages(i);
+            builder.addAllowedPackage(convertPackageIdentifierFromProto(visibleToPackage));
+        }
+
+        List<VisibleToPermissionProto> visibleToPermissionProtoList =
+                proto.getVisibleToPermissionsList();
+        for (int i = 0; i < visibleToPermissionProtoList.size(); i++) {
+            VisibleToPermissionProto visibleToPermissionProto = visibleToPermissionProtoList.get(i);
+            Set<Integer> visibleToPermissions =
+                    new ArraySet<>(visibleToPermissionProto.getPermissionsList());
+            builder.addRequiredPermissions(visibleToPermissions);
+        }
+
+        if (proto.hasPubliclyVisibleTargetPackage()) {
+            PackageIdentifierProto publiclyVisibleTargetPackage =
+                    proto.getPubliclyVisibleTargetPackage();
+            builder.setPubliclyVisibleTargetPackage(
+                    convertPackageIdentifierFromProto(publiclyVisibleTargetPackage));
+        }
+
+        return builder.build();
+    }
+
+    private static VisibilityConfigProto convertSchemaVisibilityConfigToProto(
+            @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+        VisibilityConfigProto.Builder builder = VisibilityConfigProto.newBuilder();
+
+        List<PackageIdentifier> visibleToPackages = schemaVisibilityConfig.getAllowedPackages();
+        for (int i = 0; i < visibleToPackages.size(); i++) {
+            PackageIdentifier visibleToPackage = visibleToPackages.get(i);
+            builder.addVisibleToPackages(convertPackageIdentifierToProto(visibleToPackage));
+        }
+
+        Set<Set<Integer>> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();
+        if (!visibleToPermissions.isEmpty()) {
+            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+                builder.addVisibleToPermissions(
+                        VisibleToPermissionProto.newBuilder()
+                                .addAllPermissions(allRequiredPermissions));
+            }
+        }
+
+        PackageIdentifier publicAclPackage =
+                schemaVisibilityConfig.getPubliclyVisibleTargetPackage();
+        if (publicAclPackage != null) {
+            builder.setPubliclyVisibleTargetPackage(
+                    convertPackageIdentifierToProto(publicAclPackage));
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Returns the {@link GenericDocument} for the visibility schema.
+     *
+     * @param config the configuration to populate into the document
+     */
+    @NonNull
+    public static GenericDocument createVisibilityDocument(
+            @NonNull InternalVisibilityConfig config) {
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
+                VISIBILITY_DOCUMENT_NAMESPACE,
+                config.getSchemaType(), // We are using the prefixedSchemaType to be the id
+                VISIBILITY_DOCUMENT_SCHEMA_TYPE);
+        builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
+                config.isNotDisplayedBySystem());
+        SchemaVisibilityConfig schemaVisibilityConfig = config.getVisibilityConfig();
+        List<PackageIdentifier> visibleToPackages = schemaVisibilityConfig.getAllowedPackages();
+        String[] visibleToPackageNames = new String[visibleToPackages.size()];
+        byte[][] visibleToPackageSha256Certs = new byte[visibleToPackages.size()][32];
+        for (int i = 0; i < visibleToPackages.size(); i++) {
+            visibleToPackageNames[i] = visibleToPackages.get(i).getPackageName();
+            visibleToPackageSha256Certs[i] = visibleToPackages.get(i).getSha256Certificate();
+        }
+        builder.setPropertyString(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY, visibleToPackageNames);
+        builder.setPropertyBytes(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY,
+                visibleToPackageSha256Certs);
+
+        // Generate an array of GenericDocument for VisibilityPermissionConfig.
+        Set<Set<Integer>> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions();
+        if (!visibleToPermissions.isEmpty()) {
+            GenericDocument[] permissionGenericDocs =
+                    new GenericDocument[visibleToPermissions.size()];
+            int i = 0;
+            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
+                VisibilityPermissionConfig permissionDocument =
+                        new VisibilityPermissionConfig(allRequiredPermissions);
+                permissionGenericDocs[i++] = permissionDocument.toGenericDocument();
+            }
+            builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);
+        }
+
+        // The creationTimestamp doesn't matter for Visibility documents.
+        // But to make tests pass, we set it 0 so two GenericDocuments generated from
+        // the same VisibilityConfig can be same.
+        builder.setCreationTimestampMillis(0L);
+
+        return builder.build();
+    }
+
+    /**
+     * Returns the {@link GenericDocument} for the Android V overlay schema if it is provided,
+     * null otherwise.
+     */
+    @Nullable
+    public static GenericDocument createAndroidVOverlay(
+            @NonNull InternalVisibilityConfig internalVisibilityConfig) {
+        PackageIdentifier publiclyVisibleTargetPackage =
+                internalVisibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();
+        Set<SchemaVisibilityConfig> visibleToConfigs =
+                internalVisibilityConfig.getVisibleToConfigs();
+        if (publiclyVisibleTargetPackage == null && visibleToConfigs.isEmpty()) {
+            // This config doesn't contains any Android V overlay settings
+            return null;
+        }
+
+        VisibilityConfigProto.Builder visibilityConfigProtoBuilder =
+                VisibilityConfigProto.newBuilder();
+        // Set publicAcl
+        if (publiclyVisibleTargetPackage != null) {
+            visibilityConfigProtoBuilder.setPubliclyVisibleTargetPackage(
+                    convertPackageIdentifierToProto(publiclyVisibleTargetPackage));
+        }
+
+        // Set visibleToConfigs
+        AndroidVOverlayProto.Builder androidVOverlayProtoBuilder =
+                AndroidVOverlayProto.newBuilder().setVisibilityConfig(visibilityConfigProtoBuilder);
+        if (!visibleToConfigs.isEmpty()) {
+            for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
+                VisibilityConfigProto visibleToConfigProto =
+                        convertSchemaVisibilityConfigToProto(visibleToConfig);
+                androidVOverlayProtoBuilder.addVisibleToConfigs(visibleToConfigProto);
+            }
+        }
+
+        GenericDocument.Builder<?> androidVOverlayBuilder = new GenericDocument.Builder<>(
+                ANDROID_V_OVERLAY_NAMESPACE,
+                internalVisibilityConfig.getSchemaType(),
+                ANDROID_V_OVERLAY_SCHEMA_TYPE)
+                .setPropertyBytes(
+                        VISIBILITY_PROTO_SERIALIZE_PROPERTY,
+                        androidVOverlayProtoBuilder.build().toByteArray());
+
+        // The creationTimestamp doesn't matter for Visibility documents.
+        // But to make tests pass, we set it 0 so two GenericDocuments generated from
+        // the same VisibilityConfig can be same.
+        androidVOverlayBuilder.setCreationTimestampMillis(0L);
+
+        return androidVOverlayBuilder.build();
+    }
+
+    @NonNull
+    private static PackageIdentifierProto convertPackageIdentifierToProto(
+            @NonNull PackageIdentifier packageIdentifier) {
+        return PackageIdentifierProto.newBuilder()
+                .setPackageName(packageIdentifier.getPackageName())
+                .setPackageSha256Cert(ByteString.copyFrom(packageIdentifier.getSha256Certificate()))
+                .build();
+    }
+
+    @NonNull
+    private static PackageIdentifier convertPackageIdentifierFromProto(
+            @NonNull PackageIdentifierProto packageIdentifierProto) {
+        return new PackageIdentifier(
+                packageIdentifierProto.getPackageName(),
+                packageIdentifierProto.getPackageSha256Cert().toByteArray());
+    }
+
+    private VisibilityToDocumentConverter() {}
+}
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
index 69d6a9a..075f44e 100644
--- 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
@@ -54,8 +54,11 @@
         Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(prefixedSchema);
 
-        if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
-            return true;  // Everyone is always allowed to retrieve their own data.
+        // If the caller is allowed default access to its own data, check if the calling package
+        // and the target package are the same.
+        if (callerAccess.doesCallerHaveSelfAccess()
+                && callerAccess.getCallingPackageName().equals(targetPackageName)) {
+            return true;   // Caller is allowed to retrieve its own data.
         }
         if (visibilityStore == null || visibilityChecker == null) {
             return false;  // No visibility is configured at this time; no other access possible.
diff --git a/appsearch/appsearch-platform-storage/api/current.txt b/appsearch/appsearch-platform-storage/api/current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/current.txt
+++ b/appsearch/appsearch-platform-storage/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.appsearch.platformstorage {
 
   @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.EnterpriseGlobalSearchSession!> createEnterpriseGlobalSearchSessionAsync(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 public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
diff --git a/appsearch/appsearch-platform-storage/api/restricted_current.txt b/appsearch/appsearch-platform-storage/api/restricted_current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-platform-storage/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.appsearch.platformstorage {
 
   @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.EnterpriseGlobalSearchSession!> createEnterpriseGlobalSearchSessionAsync(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 public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
diff --git a/appsearch/appsearch-platform-storage/build.gradle b/appsearch/appsearch-platform-storage/build.gradle
index 2009937..11c7bc3 100644
--- a/appsearch/appsearch-platform-storage/build.gradle
+++ b/appsearch/appsearch-platform-storage/build.gradle
@@ -31,10 +31,10 @@
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
 
-    implementation project(":appsearch:appsearch")
+    implementation(project(":appsearch:appsearch"))
+    implementation(project(":core:core"))
     implementation('androidx.collection:collection:1.2.0')
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
-    implementation("androidx.core:core:1.1.0")
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
@@ -52,5 +52,6 @@
 }
 
 android {
+    compileSdk = 35
     namespace "androidx.appsearch.platformstorage"
 }
diff --git a/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
new file mode 100644
index 0000000..0e551e4
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.platformstorage.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArraySet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SchemaValidationUtilTest {
+    static final int MAX_SECTIONS_ALLOWED = 64;
+
+    @Test
+    public void testValidate_simpleSchemas() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new BooleanPropertyConfig.Builder("boolProperty")
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new BytesPropertyConfig.Builder("byteProperty")
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema, personSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_nestedSchemas() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("org", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("sender", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("recipient", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+                .addProperty(new StringPropertyConfig.Builder("streetName")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("zipcode")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema, personSchema, addressSchema, orgSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_schemasWithValidCycle() {
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(false)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+                .addProperty(new StringPropertyConfig.Builder("streetName")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("zipcode")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {personSchema, orgSchema, addressSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_maxSections() {
+        AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+        for (int i = 0; i < MAX_SECTIONS_ALLOWED; i++) {
+            personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        Set<AppSearchSchema> schemas = new ArraySet<>();
+        schemas.add(personSchemaBuilder.build());
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas, MAX_SECTIONS_ALLOWED);
+
+        // Add one more property to bring the number of sections over the max limit
+        personSchemaBuilder.addProperty(new StringPropertyConfig.Builder(
+                "string" + MAX_SECTIONS_ALLOWED + 1)
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        schemas.clear();
+        schemas.add(personSchemaBuilder.build());
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas,
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Too many properties to be indexed");
+    }
+
+    @Test
+    public void testValidate_schemasWithInvalidCycleThrowsError() {
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {personSchema, orgSchema};
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Invalid cycle");
+    }
+
+    @Test
+    public void testValidate_unknownDocumentConfigThrowsError() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("unknown", "Unknown")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("unknown2", "Unknown")
+                        .setShouldIndexNestedProperties(false)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema};
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Undefined schema type");
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
new file mode 100644
index 0000000..b3329a1
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.platformstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+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.SearchSpecToPlatformConverter;
+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;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link EnterpriseGlobalSearchSession} which proxies to a
+ * platform {@link android.app.appsearch.EnterpriseGlobalSearchSession}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(35)
+// TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+//  BuildCompat.isAtLeastV() is removed.
[email protected]
+class EnterpriseGlobalSearchSessionImpl implements EnterpriseGlobalSearchSession {
+    private final android.app.appsearch.EnterpriseGlobalSearchSession mPlatformSession;
+    private final Executor mExecutor;
+    private final Features mFeatures;
+
+    EnterpriseGlobalSearchSessionImpl(
+            @NonNull android.app.appsearch.EnterpriseGlobalSearchSession platformSession,
+            @NonNull Executor executor,
+            @NonNull Features features) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+        mFeatures = Preconditions.checkNotNull(features);
+    }
+
+    @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);
+        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(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+            @NonNull String databaseName) {
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        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;
+    }
+}
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 4d7b3f3..b1c4f4e 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
@@ -16,15 +16,12 @@
 package androidx.appsearch.platformstorage;
 
 import android.content.Context;
-import android.content.pm.ModuleInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.os.Build;
 
-import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.appsearch.app.Features;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 /**
@@ -32,12 +29,6 @@
  * level.
  */
 final class FeaturesImpl implements Features {
-    private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
-
-    // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
-    // 0 if it is not found, or the version code if it is found.
-    private static volatile long sAppSearchVersionCode = -1;
-
     // Context is used to check mainline module version, as support varies by module version.
     private final Context mContext;
 
@@ -45,6 +36,9 @@
         mContext = Preconditions.checkNotNull(context);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     public boolean isFeatureSupported(@NonNull String feature) {
         switch (feature) {
@@ -80,23 +74,39 @@
             case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
                 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
 
-            // Beyond Android U features
+            // Android V Features
             case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
-                // TODO(b/258715421) : Update to reflect support in Android U+ once this feature has
-                // an extservices sdk that includes it.
-                // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // TODO(b/268521214) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_PARENT_TYPE:
-                // TODO(b/269295094) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
-                // TODO(b/289150947) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
-                // TODO(b/296088047) : Update when feature is ready in service-appsearch.
-                return false;
+                // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                // fall through
+            case Features.ENTERPRISE_GLOBAL_SEARCH_SESSION:
+                return BuildCompat.isAtLeastV();
+
+            // Beyond Android V Features
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+                // TODO(b/326656531) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.SCHEMA_SET_DESCRIPTION:
+                // TODO(b/326987971) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+                // TODO(b/332620561) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
+                // TODO(b/332642571) : Update when feature is ready in service-appsearch.
+                // fall through
             default:
                 return false;
         }
@@ -107,53 +117,11 @@
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             return 64;
         } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
-            // Sixty-four properties were enabled in mainline module 'aml_ase_331311020'
-            return getAppSearchVersionCode(mContext) >= 331311020 ? 64 : 16;
+            // Sixty-four properties were enabled in mainline module of the U base version
+            return AppSearchVersionUtil.getAppSearchVersionCode(mContext)
+                    >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE ? 64 : 16;
         } else {
             return 16;
         }
     }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    private static long getAppSearchVersionCode(Context context) {
-        if (sAppSearchVersionCode != -1) {
-            return sAppSearchVersionCode;
-        }
-        synchronized (FeaturesImpl.class) {
-            // Check again in case it was assigned while waiting
-            if (sAppSearchVersionCode == -1) {
-                long appsearchVersionCode = 0;
-                try {
-                    PackageManager packageManager = context.getPackageManager();
-                    String appSearchPackageName =
-                            ApiHelperForQ.getAppSearchPackageName(packageManager);
-                    if (appSearchPackageName != null) {
-                        PackageInfo pInfo = packageManager
-                                .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
-                        appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
-                    }
-                } catch (PackageManager.NameNotFoundException e) {
-                    // Module not installed
-                }
-                sAppSearchVersionCode = appsearchVersionCode;
-            }
-        }
-        return sAppSearchVersionCode;
-    }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    private static class ApiHelperForQ {
-        @DoNotInline
-        static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
-            return pInfo.getLongVersionCode();
-        }
-
-        @DoNotInline
-        static String getAppSearchPackageName(PackageManager packageManager)
-                throws PackageManager.NameNotFoundException {
-            ModuleInfo appSearchModule =
-                    packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
-            return appSearchModule.getPackageName();
-        }
-    }
 }
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 26c2711c..a5cc45d 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
@@ -47,6 +47,7 @@
 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;
@@ -103,6 +104,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public SearchResults search(
@@ -131,6 +135,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     @Override
     public ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
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 c1ca5d4..0a586c5 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
@@ -20,20 +20,25 @@
 import android.content.Context;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter;
 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.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.function.Consumer;
 
 /**
  * An AppSearch storage system which stores data in the central AppSearch service, available on
@@ -201,7 +206,8 @@
     // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     /**
      * Opens a new {@link AppSearchSession} on this storage.
@@ -224,7 +230,7 @@
                     if (result.isSuccess()) {
                         future.set(
                                 new SearchSessionImpl(result.getResultValue(), context.mExecutor,
-                                        new FeaturesImpl(context.mContext)));
+                                        context.mContext));
                     } else {
                         // Without the SuppressLint annotation on the method, this line causes a
                         // lint error because getResultCode isn't defined as returning a value from
@@ -266,4 +272,59 @@
                 });
         return future;
     }
+
+    /**
+     * Opens a new {@link EnterpriseGlobalSearchSession} on this storage.
+     */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
+    @RequiresApi(35)
+    @SuppressLint("WrongConstant")
+    @NonNull
+    public static ListenableFuture<EnterpriseGlobalSearchSession>
+            createEnterpriseGlobalSearchSessionAsync(@NonNull GlobalSearchContext context) {
+        if (!BuildCompat.isAtLeastV()) {
+            throw new UnsupportedOperationException(
+                    Features.ENTERPRISE_GLOBAL_SEARCH_SESSION
+                            + " is not supported on this AppSearch implementation");
+        }
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture<EnterpriseGlobalSearchSession> future = ResolvableFuture.create();
+        ApiHelperForV.createEnterpriseGlobalSearchSession(
+                appSearchManager,
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(new EnterpriseGlobalSearchSessionImpl(
+                                result.getResultValue(), context.mExecutor,
+                                new FeaturesImpl(context.mContext)));
+                    } else {
+                        // Without the SuppressLint annotation on the method, this line causes a
+                        // lint error because getResultCode isn't defined as returning a value from
+                        // AppSearchResult.ResultCode
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void createEnterpriseGlobalSearchSession(@NonNull AppSearchManager appSearchManager,
+                @NonNull Executor executor,
+                @NonNull Consumer<android.app.appsearch.AppSearchResult<
+                        android.app.appsearch.EnterpriseGlobalSearchSession>> callback) {
+            appSearchManager.createEnterpriseGlobalSearchSession(executor, callback);
+        }
+    }
 }
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 d0514b0..d766182 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
@@ -15,8 +15,11 @@
  */
 package androidx.appsearch.platformstorage;
 
+import static androidx.appsearch.platformstorage.util.SchemaValidationUtil.checkSchemasAreValidOrThrow;
+
 import android.annotation.SuppressLint;
 import android.app.appsearch.AppSearchResult;
+import android.content.Context;
 import android.os.Build;
 
 import androidx.annotation.DoNotInline;
@@ -40,6 +43,7 @@
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.exceptions.IllegalSchemaException;
 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
@@ -49,8 +53,10 @@
 import androidx.appsearch.platformstorage.converter.SearchSuggestionResultToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.SearchSuggestionSpecToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
 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;
@@ -70,22 +76,39 @@
 class SearchSessionImpl implements AppSearchSession {
     private final android.app.appsearch.AppSearchSession mPlatformSession;
     private final Executor mExecutor;
+    private final Context mContext;
     private final Features mFeatures;
 
     SearchSessionImpl(
             @NonNull android.app.appsearch.AppSearchSession platformSession,
             @NonNull Executor executor,
-            @NonNull Features features) {
+            @NonNull Context context) {
         mPlatformSession = Preconditions.checkNotNull(platformSession);
         mExecutor = Preconditions.checkNotNull(executor);
-        mFeatures = Preconditions.checkNotNull(features);
+        mContext = Preconditions.checkNotNull(context);
+        mFeatures = new FeaturesImpl(mContext);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
+        if (needsSchemaValidation()) {
+            try {
+                checkSchemasAreValidOrThrow(request.getSchemas(),
+                        getFeatures().getMaxIndexedProperties());
+            } catch (IllegalSchemaException e) {
+                future.setException(
+                        new AppSearchException(
+                                androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT,
+                                e.getMessage()));
+                return future;
+            }
+        }
         mPlatformSession.setSchema(
                 SetSchemaRequestToPlatformConverter.toPlatformSetSchemaRequest(request),
                 mExecutor,
@@ -97,6 +120,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
@@ -121,6 +147,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
@@ -149,6 +178,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public SearchResults search(
@@ -163,6 +195,9 @@
         return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     @Override
     public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
@@ -216,6 +251,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @SuppressLint("WrongConstant")
     @Override
     @NonNull
@@ -318,6 +356,17 @@
         mPlatformSession.close();
     }
 
+    private boolean needsSchemaValidation() {
+        long appsearchVersionCode = AppSearchVersionUtil.getAppSearchVersionCode(mContext);
+        // Due to b/300135897, we'd like to validate the schema before sending the setSchema
+        // request to IcingLib on some versions of AppSearch.
+        // For these versions, IcingLib and AppSearch would crash if we try to set an
+        // invalid schema where the number of sections in a schema type exceeds the maximum
+        // limit.
+        return appsearchVersionCode >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE
+                && appsearchVersionCode < AppSearchVersionUtil.APPSEARCH_M2023_11_VERSION_CODE;
+    }
+
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static class ApiHelperForU {
         private ApiHelperForU() {
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 3775ab7..01882d6 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
@@ -21,6 +21,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
@@ -87,6 +89,10 @@
                     platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]);
                 }
                 platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);
+            } else if (property instanceof EmbeddingVector[]) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
             } else {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
@@ -143,6 +149,8 @@
                 }
                 jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
             } else {
+                // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+                //  available in platform.
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
                                 property.getClass().toString()));
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
index 2fb4a40..577673e 100644
--- 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
@@ -24,9 +24,14 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -43,13 +48,16 @@
      * Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
      * {@link GetSchemaResponse}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static GetSchemaResponse toJetpackGetSchemaResponse(
             @NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
         Preconditions.checkNotNull(platformResponse);
         GetSchemaResponse.Builder jetpackBuilder = new GetSchemaResponse.Builder();
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            // Android API level in S-v2 and lower won't have any supported feature.
+            // Android API level in S-v2 and lower won't have visibility information.
             jetpackBuilder.setVisibilitySettingSupported(false);
         }
         for (android.app.appsearch.AppSearchSchema platformSchema : platformResponse.getSchemas()) {
@@ -72,6 +80,29 @@
                         entry.getValue());
             }
         }
+
+        if (BuildCompat.isAtLeastV()) {
+            // Convert publicly visible schemas
+            Map<String, PackageIdentifier> publiclyVisibleSchemas =
+                    ApiHelperForV.getPubliclyVisibleSchemas(platformResponse);
+            if (!publiclyVisibleSchemas.isEmpty()) {
+                for (Map.Entry<String, PackageIdentifier> entry :
+                        publiclyVisibleSchemas.entrySet()) {
+                    jetpackBuilder.setPubliclyVisibleSchema(entry.getKey(), entry.getValue());
+                }
+            }
+
+            // Convert schemas visible to configs
+            Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+                    ApiHelperForV.getSchemasVisibleToConfigs(platformResponse);
+            if (!schemasVisibleToConfigs.isEmpty()) {
+                for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                        schemasVisibleToConfigs.entrySet()) {
+                    jetpackBuilder.setSchemaTypeVisibleToConfigs(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
         return jetpackBuilder.build();
     }
 
@@ -130,4 +161,95 @@
             return platformResponse.getRequiredPermissionsForSchemaTypeVisibility();
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        @NonNull
+        static Map<String, PackageIdentifier> getPubliclyVisibleSchemas(
+                android.app.appsearch.GetSchemaResponse platformResponse) {
+            Map<String, android.app.appsearch.PackageIdentifier> platformPubliclyVisibleSchemas =
+                    platformResponse.getPubliclyVisibleSchemas();
+            if (platformPubliclyVisibleSchemas.isEmpty()) {
+                return Collections.emptyMap();
+            }
+            Map<String, PackageIdentifier> jetpackPubliclyVisibleSchemas =
+                    new ArrayMap<>(platformPubliclyVisibleSchemas.size());
+            for (Map.Entry<String, android.app.appsearch.PackageIdentifier> entry :
+                    platformPubliclyVisibleSchemas.entrySet()) {
+                jetpackPubliclyVisibleSchemas.put(
+                        entry.getKey(),
+                        new PackageIdentifier(
+                                entry.getValue().getPackageName(),
+                                entry.getValue().getSha256Certificate()));
+            }
+            return jetpackPubliclyVisibleSchemas;
+        }
+
+        @DoNotInline
+        @NonNull
+        static Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs(
+                android.app.appsearch.GetSchemaResponse platformResponse) {
+            Map<String, Set<android.app.appsearch.SchemaVisibilityConfig>>
+                    platformSchemasVisibleToConfigs =
+                    platformResponse.getSchemaTypesVisibleToConfigs();
+            if (platformSchemasVisibleToConfigs.isEmpty()) {
+                return Collections.emptyMap();
+            }
+            Map<String, Set<SchemaVisibilityConfig>> jetpackSchemasVisibleToConfigs =
+                    new ArrayMap<>(platformSchemasVisibleToConfigs.size());
+            for (Map.Entry<String, Set<android.app.appsearch.SchemaVisibilityConfig>> entry :
+                    platformSchemasVisibleToConfigs.entrySet()) {
+                Set<SchemaVisibilityConfig> jetpackConfigPerType =
+                        new ArraySet<>(entry.getValue().size());
+                for (android.app.appsearch.SchemaVisibilityConfig platformConfigPerType :
+                        entry.getValue()) {
+                    SchemaVisibilityConfig jetpackConfig =
+                            toJetpackSchemaVisibilityConfig(platformConfigPerType);
+                    jetpackConfigPerType.add(jetpackConfig);
+                }
+                jetpackSchemasVisibleToConfigs.put(entry.getKey(), jetpackConfigPerType);
+            }
+            return jetpackSchemasVisibleToConfigs;
+        }
+
+        /**
+         * Translates a platform {@link android.app.appsearch.SchemaVisibilityConfig} into a jetpack
+         * {@link SchemaVisibilityConfig}.
+         */
+        @NonNull
+        private static SchemaVisibilityConfig toJetpackSchemaVisibilityConfig(
+                @NonNull android.app.appsearch.SchemaVisibilityConfig platformConfig) {
+            Preconditions.checkNotNull(platformConfig);
+            SchemaVisibilityConfig.Builder jetpackBuilder = new SchemaVisibilityConfig.Builder();
+
+            // Translate allowedPackages
+            List<android.app.appsearch.PackageIdentifier> allowedPackages =
+                    platformConfig.getAllowedPackages();
+            for (int i = 0; i < allowedPackages.size(); i++) {
+                jetpackBuilder.addAllowedPackage(new PackageIdentifier(
+                        allowedPackages.get(i).getPackageName(),
+                        allowedPackages.get(i).getSha256Certificate()));
+            }
+
+            // Translate requiredPermissions
+            for (Set<Integer> requiredPermissions : platformConfig.getRequiredPermissions()) {
+                jetpackBuilder.addRequiredPermissions(requiredPermissions);
+            }
+
+            // Translate publiclyVisibleTargetPackage
+            android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage =
+                    platformConfig.getPubliclyVisibleTargetPackage();
+            if (publiclyVisibleTargetPackage != null) {
+                jetpackBuilder.setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+
+            return jetpackBuilder.build();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
index 83584df..1201088 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
@@ -23,6 +23,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.JoinSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 /**
@@ -38,6 +39,9 @@
      */
     @SuppressLint("WrongConstant")
     @NonNull
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     public static android.app.appsearch.JoinSpec toPlatformJoinSpec(@NonNull JoinSpec jetpackSpec) {
         Preconditions.checkNotNull(jetpackSpec);
         return new android.app.appsearch.JoinSpec.Builder(jetpackSpec.getChildPropertyExpression())
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
index f4b2d43..35a4226 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
@@ -18,6 +18,7 @@
 
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
@@ -27,6 +28,7 @@
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.ReportUsageRequest;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -45,16 +47,37 @@
      * Translates a jetpack {@link PutDocumentsRequest} into a platform
      * {@link android.app.appsearch.PutDocumentsRequest}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest(
             @NonNull PutDocumentsRequest jetpackRequest) {
         Preconditions.checkNotNull(jetpackRequest);
         android.app.appsearch.PutDocumentsRequest.Builder platformBuilder =
                 new android.app.appsearch.PutDocumentsRequest.Builder();
+        // Convert normal generic documents.
         for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
             platformBuilder.addGenericDocuments(
                     GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument));
         }
+        // Convert taken action generic documents.
+        for (GenericDocument jetpackTakenActionGenericDocument :
+                jetpackRequest.getTakenActionGenericDocuments()) {
+            if (BuildCompat.isAtLeastV()) {
+                ApiHelperForV.addTakenActionGenericDocuments(
+                        platformBuilder,
+                        GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                                jetpackTakenActionGenericDocument));
+            } else {
+                // This version of platform-storage doesn't support the dedicated
+                // addTakenActionGenericDocuments API, but we can still add them to the index via
+                // the put API (just without logging).
+                platformBuilder.addGenericDocuments(
+                        GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                                jetpackTakenActionGenericDocument));
+            }
+        }
         return platformBuilder.build();
     }
 
@@ -71,7 +94,7 @@
                         jetpackRequest.getNamespace())
                         .addIds(jetpackRequest.getIds());
         for (Map.Entry<String, List<String>> projection :
-                jetpackRequest.getProjectionsInternal().entrySet()) {
+                jetpackRequest.getProjections().entrySet()) {
             platformBuilder.addProjection(projection.getKey(), projection.getValue());
         }
         return platformBuilder.build();
@@ -122,4 +145,24 @@
                 .setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis())
                 .build();
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addTakenActionGenericDocuments(
+                android.app.appsearch.PutDocumentsRequest.Builder platformBuilder,
+                android.app.appsearch.GenericDocument platformTakenActionGenericDocument) {
+            try {
+                platformBuilder.addTakenActionGenericDocuments(platformTakenActionGenericDocument);
+            } catch (android.app.appsearch.exceptions.AppSearchException e) {
+                // This method incorrectly declares that it throws AppSearchException, whereas in
+                // fact there's nothing in its implementation that would do so. Suppress it here
+                // instead of piping all the way through the stack.
+                throw new RuntimeException(
+                        "Unexpected AppSearchException which should not be possible", e);
+            }
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
index 5cf9946..b6ebd2d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
@@ -28,8 +28,10 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.Features;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -46,23 +48,36 @@
      * Translates a jetpack {@link AppSearchSchema} into a platform
      * {@link android.app.appsearch.AppSearchSchema}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.AppSearchSchema toPlatformSchema(
             @NonNull AppSearchSchema jetpackSchema) {
         Preconditions.checkNotNull(jetpackSchema);
         android.app.appsearch.AppSearchSchema.Builder platformBuilder =
                 new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());
+        if (!jetpackSchema.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
+        if (!jetpackSchema.getParentTypes().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
+                        + " is not available on this AppSearch implementation.");
+            }
+            List<String> parentTypes = jetpackSchema.getParentTypes();
+            for (int i = 0; i < parentTypes.size(); i++) {
+                ApiHelperForV.addParentType(platformBuilder, parentTypes.get(i));
+            }
+        }
         List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty =
                     toPlatformProperty(properties.get(i));
             platformBuilder.addProperty(platformProperty);
         }
-        if (!jetpackSchema.getParentTypes().isEmpty()) {
-            // TODO(b/269295094): Remove this once polymorphism becomes available.
-            throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
-                    + " is not available on this AppSearch implementation.");
-        }
         return platformBuilder.build();
     }
 
@@ -70,6 +85,9 @@
      * Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
      * {@link AppSearchSchema}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static AppSearchSchema toJetpackSchema(
             @NonNull android.app.appsearch.AppSearchSchema platformSchema) {
@@ -78,22 +96,36 @@
                 new AppSearchSchema.Builder(platformSchema.getSchemaType());
         List<android.app.appsearch.AppSearchSchema.PropertyConfig> properties =
                 platformSchema.getProperties();
+        // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+        // available in platform.
+        if (BuildCompat.isAtLeastV()) {
+            List<String> parentTypes = ApiHelperForV.getParentTypes(platformSchema);
+            for (int i = 0; i < parentTypes.size(); i++) {
+                jetpackBuilder.addParentType(parentTypes.get(i));
+            }
+        }
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
             jetpackBuilder.addProperty(jetpackProperty);
         }
-        // TODO(b/269295094): Call jetpackBuilder.addParentType() to add parent types once
-        //  polymorphism becomes available in platform.
         return jetpackBuilder.build();
     }
 
     // Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
             @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
         Preconditions.checkNotNull(jetpackProperty);
+        if (!jetpackProperty.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
         if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
             AppSearchSchema.StringPropertyConfig stringProperty =
                     (AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -110,12 +142,6 @@
                         TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
             }
 
-            if (stringProperty.getDeletionPropagation()) {
-                // TODO(b/268521214): Update once deletion propagation is available.
-                throw new UnsupportedOperationException("Setting deletion propagation is not "
-                        + "supported on this AppSearch implementation.");
-            }
-
             if (stringProperty.getJoinableValueType()
                     == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@@ -163,18 +189,26 @@
         } else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) {
             AppSearchSchema.DocumentPropertyConfig documentProperty =
                     (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
+            android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder platformBuilder =
+                    new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
+                            documentProperty.getName(), documentProperty.getSchemaType())
+                            .setCardinality(documentProperty.getCardinality())
+                            .setShouldIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties());
             if (!documentProperty.getIndexableNestedProperties().isEmpty()) {
-                // TODO(b/289150947): Update and set list once indexable-nested-properties-list is
-                //  available.
-                throw new UnsupportedOperationException(
-                        "DocumentPropertyConfig.addIndexableNestedProperties is not supported on "
-                                + "this AppSearch implementation.");
+                if (!BuildCompat.isAtLeastV()) {
+                    throw new UnsupportedOperationException(
+                            "DocumentPropertyConfig.addIndexableNestedProperties is not supported "
+                                    + "on this AppSearch implementation.");
+                }
+                ApiHelperForV.addIndexableNestedProperties(
+                        platformBuilder, documentProperty.getIndexableNestedProperties());
             }
-            return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
-                    documentProperty.getName(), documentProperty.getSchemaType())
-                    .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
-                    .build();
+            return platformBuilder.build();
+        } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            // TODO(b/326656531): Remove this once embedding search APIs are available.
+            throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                    + " is not available on this AppSearch implementation.");
         } else {
             throw new IllegalArgumentException(
                     "Invalid dataType: " + jetpackProperty.getDataType());
@@ -184,6 +218,9 @@
     // Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     private static AppSearchSchema.PropertyConfig toJetpackProperty(
             @NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
@@ -201,6 +238,8 @@
                 jetpackBuilder.setJoinableValueType(
                         ApiHelperForU.getJoinableValueType(stringProperty));
             }
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return jetpackBuilder.build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {
@@ -213,19 +252,27 @@
                 jetpackBuilder.setIndexingType(
                         ApiHelperForU.getIndexingType(longProperty));
             }
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return jetpackBuilder.build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
@@ -233,15 +280,24 @@
                 instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) {
             android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty =
                     (android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;
-            return new AppSearchSchema.DocumentPropertyConfig.Builder(
-                    documentProperty.getName(),
-                    documentProperty.getSchemaType())
-                    .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
-                    .build();
-            // TODO(b/289150947): Add the indexable_nested_properties_list once it becomes
-            //  available in platform.
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
+            AppSearchSchema.DocumentPropertyConfig.Builder jetpackBuilder =
+                    new AppSearchSchema.DocumentPropertyConfig.Builder(
+                            documentProperty.getName(),
+                            documentProperty.getSchemaType())
+                            .setCardinality(documentProperty.getCardinality())
+                            .setShouldIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties());
+            if (BuildCompat.isAtLeastV()) {
+                List<String> indexableNestedProperties =
+                        ApiHelperForV.getIndexableNestedProperties(documentProperty);
+                jetpackBuilder.addIndexableNestedProperties(indexableNestedProperties);
+            }
+            return jetpackBuilder.build();
         } else {
+            // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+            //  available in platform.
             throw new IllegalArgumentException(
                     "Invalid property type " + platformProperty.getClass()
                             + ": " + platformProperty);
@@ -288,4 +344,38 @@
             return longPropertyConfig.getIndexingType();
         }
     }
+
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        @SuppressLint("NewApi")
+        static void addParentType(
+                android.app.appsearch.AppSearchSchema.Builder platformBuilder,
+                @NonNull String parentSchemaType) {
+            platformBuilder.addParentType(parentSchemaType);
+        }
+
+        @DoNotInline
+        static void addIndexableNestedProperties(
+                android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder
+                        platformBuilder,
+                @NonNull Collection<String> indexableNestedProperties) {
+            platformBuilder.addIndexableNestedProperties(indexableNestedProperties);
+        }
+
+        @DoNotInline
+        static List<String> getParentTypes(android.app.appsearch.AppSearchSchema platformSchema) {
+            return platformSchema.getParentTypes();
+        }
+
+        @DoNotInline
+        static List<String> getIndexableNestedProperties(
+                android.app.appsearch.AppSearchSchema.DocumentPropertyConfig
+                        platformDocumentProperty) {
+            return platformDocumentProperty.getIndexableNestedProperties();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
index 47bff5a..38c867c 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -26,6 +26,7 @@
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -46,6 +47,9 @@
     // Most jetpackSearchSpec.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
             @NonNull SearchSpec jetpackSearchSpec) {
@@ -77,15 +81,12 @@
                 .setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
                 .setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
         if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {
-            // TODO(b/258715421): Add Build.VERSION.SDK_INT condition once there is an extservices
-            // sdk that includes SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA.
-            if (true) {
-                if ((jetpackSearchSpec.getResultGroupingTypeFlags()
-                        & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0) {
-                    throw new UnsupportedOperationException(
+            if ((jetpackSearchSpec.getResultGroupingTypeFlags()
+                    & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0
+                    && !BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                            + " is not available on this AppSearch implementation.");
-                }
+                                + " is not available on this AppSearch implementation.");
             }
             platformBuilder.setResultGrouping(
                     jetpackSearchSpec.getResultGroupingTypeFlags(),
@@ -107,6 +108,7 @@
         }
 
         if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+            // Copy U features
             if (jetpackSearchSpec.isNumericSearchEnabled()
                     || jetpackSearchSpec.isVerbatimSearchEnabled()
                     || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
@@ -118,6 +120,27 @@
                 }
                 ApiHelperForU.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
             }
+            // Copy V features
+            if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+                if (!BuildCompat.isAtLeastV()) {
+                    throw new UnsupportedOperationException(
+                            Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                                    + " is not available on this AppSearch implementation.");
+                }
+                ApiHelperForV.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
+            }
+            // Copy beyond-V features
+            if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+                    || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
+            }
+            if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+                // TODO(b/332620561): Remove this once 'tokenize' is supported.
+                throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                        + " is not available on this AppSearch implementation.");
+            }
         }
 
         if (jetpackSearchSpec.getJoinSpec() != null) {
@@ -129,9 +152,29 @@
         }
 
         if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
-            // TODO(b/296088047): Remove this once property filters become available.
-            throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
-                    + " is not available on this AppSearch implementation.");
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                        + " is not available on this AppSearch implementation.");
+            }
+            ApiHelperForV.addFilterProperties(
+                    platformBuilder, jetpackSearchSpec.getFilterProperties());
+        }
+
+        if (jetpackSearchSpec.getSearchSourceLogTag() != null) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+                                + " is not available on this AppSearch implementation.");
+            }
+            ApiHelperForV.setSearchSourceLogTag(
+                    platformBuilder, jetpackSearchSpec.getSearchSourceLogTag());
+        }
+
+        if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+            // TODO(b/332642571): Remove this once informational ranking expressions are available.
+            throw new UnsupportedOperationException(
+                    Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                            + " are not available on this AppSearch implementation.");
         }
         return platformBuilder.build();
     }
@@ -143,6 +186,9 @@
         }
 
         @DoNotInline
+        // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+        //  BuildCompat.isAtLeastV() is removed.
+        @BuildCompat.PrereleaseSdkCheck
         static void setJoinSpec(@NonNull android.app.appsearch.SearchSpec.Builder builder,
                 JoinSpec jetpackJoinSpec) {
             builder.setJoinSpec(JoinSpecToPlatformConverter.toPlatformJoinSpec(jetpackJoinSpec));
@@ -176,4 +222,34 @@
             }
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addFilterProperties(
+                @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+                Map<String, List<String>> properties) {
+            for (Map.Entry<String, List<String>> entry : properties.entrySet()) {
+                platformBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+            }
+        }
+
+        @DoNotInline
+        static void copyEnabledFeatures(
+                @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+                @NonNull SearchSpec jetpackSpec) {
+            if (jetpackSpec.isListFilterHasPropertyFunctionEnabled()) {
+                platformBuilder.setListFilterHasPropertyFunctionEnabled(true);
+            }
+        }
+
+        @DoNotInline
+        static void setSearchSourceLogTag(
+                android.app.appsearch.SearchSpec.Builder platformBuilder,
+                String searchSourceLogTag) {
+            platformBuilder.setSearchSourceLogTag(searchSourceLogTag);
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
index 5479749..ed4a2b2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
@@ -19,12 +19,16 @@
 import android.annotation.SuppressLint;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -44,6 +48,9 @@
     // methods are not defined as returning the same constants as the corresponding setter
     // expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SearchSuggestionSpec toPlatformSearchSuggestionSpec(
             @NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
@@ -62,6 +69,32 @@
             platformBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
                     documentIdFilters.getValue());
         }
+
+        Map<String, List<String>> jetpackFilterProperties =
+                jetpackSearchSuggestionSpec.getFilterProperties();
+        if (!jetpackFilterProperties.isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                        + " is not available on this AppSearch implementation.");
+            }
+            for (Map.Entry<String, List<String>> entry : jetpackFilterProperties.entrySet()) {
+                ApiHelperForV.addFilterProperties(
+                        platformBuilder, entry.getKey(), entry.getValue());
+            }
+        }
         return platformBuilder.build();
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addFilterProperties(
+                android.app.appsearch.SearchSuggestionSpec.Builder platformBuilder,
+                String schema,
+                Collection<String> propertyPaths) {
+            platformBuilder.addFilterProperties(schema, propertyPaths);
+        }
+    }
 }
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 d03d22c..85a1361 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
@@ -26,10 +26,13 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -47,6 +50,9 @@
      * Translates a jetpack {@link SetSchemaRequest} into a platform
      * {@link android.app.appsearch.SetSchemaRequest}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
             @NonNull SetSchemaRequest jetpackRequest) {
@@ -86,6 +92,38 @@
                 }
             }
         }
+
+        if (!jetpackRequest.getPubliclyVisibleSchemas().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        "Publicly visible schema are not supported on this AppSearch "
+                                + "implementation.");
+            }
+            for (Map.Entry<String, PackageIdentifier> entry :
+                    jetpackRequest.getPubliclyVisibleSchemas().entrySet()) {
+                PackageIdentifier publiclyVisibleTargetPackage = entry.getValue();
+                ApiHelperForV.setPubliclyVisibleSchema(
+                        platformBuilder,
+                        entry.getKey(),
+                        new android.app.appsearch.PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+        }
+
+        if (!jetpackRequest.getSchemasVisibleToConfigs().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        "Schema visible to config are not supported on this AppSearch "
+                                + "implementation.");
+            }
+            for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                    jetpackRequest.getSchemasVisibleToConfigs().entrySet()) {
+                ApiHelperForV.addSchemaTypeVisibleToConfig(
+                        platformBuilder, entry.getKey(), entry.getValue());
+            }
+        }
+
         for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
             Migrator jetpackMigrator = entry.getValue();
             android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
@@ -176,4 +214,66 @@
             platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(schemaType, permissions);
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void setPubliclyVisibleSchema(
+                android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+                String schemaType,
+                android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage) {
+            platformBuilder.setPubliclyVisibleSchema(schemaType, publiclyVisibleTargetPackage);
+        }
+
+        @DoNotInline
+        public static void addSchemaTypeVisibleToConfig(
+                android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+                String schemaType,
+                Set<SchemaVisibilityConfig> jetpackConfigs) {
+            for (SchemaVisibilityConfig jetpackConfig : jetpackConfigs) {
+                android.app.appsearch.SchemaVisibilityConfig platformConfig =
+                        toPlatformSchemaVisibilityConfig(jetpackConfig);
+                platformBuilder.addSchemaTypeVisibleToConfig(schemaType, platformConfig);
+            }
+        }
+
+        /**
+         * Translates a jetpack {@link SchemaVisibilityConfig} into a platform
+         * {@link android.app.appsearch.SchemaVisibilityConfig}.
+         */
+        @NonNull
+        private static android.app.appsearch.SchemaVisibilityConfig
+                toPlatformSchemaVisibilityConfig(@NonNull SchemaVisibilityConfig jetpackConfig) {
+            Preconditions.checkNotNull(jetpackConfig);
+            android.app.appsearch.SchemaVisibilityConfig.Builder platformBuilder =
+                    new android.app.appsearch.SchemaVisibilityConfig.Builder();
+
+            // Translate allowedPackages
+            List<PackageIdentifier> allowedPackages = jetpackConfig.getAllowedPackages();
+            for (int i = 0; i < allowedPackages.size(); i++) {
+                platformBuilder.addAllowedPackage(new android.app.appsearch.PackageIdentifier(
+                        allowedPackages.get(i).getPackageName(),
+                        allowedPackages.get(i).getSha256Certificate()));
+            }
+
+            // Translate requiredPermissions
+            for (Set<Integer> requiredPermissions : jetpackConfig.getRequiredPermissions()) {
+                platformBuilder.addRequiredPermissions(requiredPermissions);
+            }
+
+            // Translate publiclyVisibleTargetPackage
+            PackageIdentifier publiclyVisibleTargetPackage =
+                    jetpackConfig.getPubliclyVisibleTargetPackage();
+            if (publiclyVisibleTargetPackage != null) {
+                platformBuilder.setPubliclyVisibleTargetPackage(
+                        new android.app.appsearch.PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+
+            return platformBuilder.build();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
new file mode 100644
index 0000000..3d778ec
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.platformstorage.util;
+
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for retrieving platform AppSearch's module version code.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchVersionUtil {
+    public static final long APPSEARCH_U_BASE_VERSION_CODE = 331311020;
+    public static final long APPSEARCH_M2023_11_VERSION_CODE = 341113000;
+
+    private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
+
+    // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
+    // 0 if it is not found, or the version code if it is found.
+    private static volatile long sAppSearchVersionCode = -1;
+
+    private AppSearchVersionUtil() {
+    }
+
+    /**
+     * Returns AppSearch's version code from the context.
+     */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    public static long getAppSearchVersionCode(@NonNull Context context) {
+        Preconditions.checkNotNull(context);
+        if (sAppSearchVersionCode != -1) {
+            return sAppSearchVersionCode;
+        }
+        synchronized (AppSearchVersionUtil.class) {
+            // Check again in case it was assigned while waiting
+            if (sAppSearchVersionCode == -1) {
+                long appsearchVersionCode = 0;
+                try {
+                    PackageManager packageManager = context.getPackageManager();
+                    String appSearchPackageName =
+                            ApiHelperForQ.getAppSearchPackageName(packageManager);
+                    if (appSearchPackageName != null) {
+                        PackageInfo pInfo = packageManager
+                                .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
+                        appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
+                    }
+                } catch (PackageManager.NameNotFoundException e) {
+                    // Module not installed
+                }
+                sAppSearchVersionCode = appsearchVersionCode;
+            }
+        }
+        return sAppSearchVersionCode;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private static class ApiHelperForQ {
+        @DoNotInline
+        static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
+            return pInfo.getLongVersionCode();
+        }
+
+        @DoNotInline
+        static String getAppSearchPackageName(PackageManager packageManager)
+                throws PackageManager.NameNotFoundException {
+            ModuleInfo appSearchModule =
+                    packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
+            return appSearchModule.getPackageName();
+        }
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
new file mode 100644
index 0000000..7a98515b
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.platformstorage.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema validation.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaValidationUtil {
+    private SchemaValidationUtil() {
+    }
+
+    /**
+     * Checks that the set of AppSearch schemas form valid schema-type definitions, and do not
+     * exceed the maximum number of indexed properties allowed.
+     *
+     * @param appSearchSchemas   Set of AppSearch Schemas for a
+     *                           {@link androidx.appsearch.app.SetSchemaRequest}.
+     * @param maxSectionsAllowed The maximum number of sections allowed per AppSearch schema.
+     * @throws IllegalSchemaException if any schema in the set is invalid. A schema is invalid if
+     *                                it contains an invalid cycle, has an undefined document
+     *                                property config, or exceeds the
+     *                                maximum number of sections allowed.
+     */
+    public static void checkSchemasAreValidOrThrow(@NonNull Set<AppSearchSchema> appSearchSchemas,
+            int maxSectionsAllowed) throws IllegalSchemaException {
+        Map<String, AppSearchSchema> knownSchemas = new ArrayMap<>();
+        for (AppSearchSchema schema : appSearchSchemas) {
+            knownSchemas.put(schema.getSchemaType(), schema);
+        }
+        Map<String, Integer> cachedNumSectionsMap = new ArrayMap<>();
+        for (AppSearchSchema schema : appSearchSchemas) {
+            // Check that the number of sections is below the max allowed
+            int numSections = getNumSectionsInSchemaOrThrow(schema, knownSchemas,
+                    cachedNumSectionsMap, new ArraySet<>());
+            if (numSections > maxSectionsAllowed) {
+                throw new IllegalSchemaException(
+                        "Too many properties to be indexed, max " + "number of properties allowed: "
+                                + maxSectionsAllowed);
+            }
+        }
+    }
+
+    /**
+     * Returns the number of indexes sections in a given AppSearch schema.
+     *
+     * @param schema                    The AppSearch schema to get the number of sections for.
+     * @param knownSchemas              Map of known schema-type strings to their corresponding
+     *                                  schemas.
+     * @param cachedNumSectionsInSchema Map of the cached number of sections in schemas which have
+     *                                  already been expanded.
+     * @param visitedSchemaTypes        Set of schemas that have already been expanded as parents
+     *                                  of the current schema.
+     * @throws IllegalSchemaException if the schema contains an invalid cycle, or contains a
+     *                                DocumentPropertyConfig where the config's schema type is
+     *                                unknown.
+     */
+    private static int getNumSectionsInSchemaOrThrow(@NonNull AppSearchSchema schema,
+            @NonNull Map<String, AppSearchSchema> knownSchemas,
+            @NonNull Map<String, Integer> cachedNumSectionsInSchema,
+            @NonNull Set<String> visitedSchemaTypes)
+            throws IllegalSchemaException {
+        String schemaType = schema.getSchemaType();
+        if (visitedSchemaTypes.contains(schemaType)) {
+            // We've hit an illegal cycle where all DocumentPropertyConfigs set
+            // shouldIndexNestedProperties = true.
+            throw new IllegalSchemaException(
+                    "Invalid cycle detected in schema type configs. '" + schemaType
+                            + "' references itself.");
+        }
+        if (cachedNumSectionsInSchema.containsKey(schemaType)) {
+            // We've already calculated and cached the number of sections in this AppSearch schema,
+            // just return this value.
+            return cachedNumSectionsInSchema.get(schemaType);
+        }
+
+        visitedSchemaTypes.add(schemaType);
+        int numSections = 0;
+        for (PropertyConfig property : schema.getProperties()) {
+            if (property.getDataType() == PropertyConfig.DATA_TYPE_DOCUMENT) {
+                DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) property;
+                String docPropertySchemaType = documentProperty.getSchemaType();
+                if (!knownSchemas.containsKey(docPropertySchemaType)) {
+                    // The schema type that this document property config is referring to
+                    // does not exist in the provided schemas
+                    throw new IllegalSchemaException(
+                            "Undefined schema type: " + docPropertySchemaType);
+                }
+                if (!documentProperty.shouldIndexNestedProperties()) {
+                    numSections += documentProperty.getIndexableNestedProperties().size();
+                } else {
+                    numSections += getNumSectionsInSchemaOrThrow(
+                            knownSchemas.get(docPropertySchemaType), knownSchemas,
+                            cachedNumSectionsInSchema, visitedSchemaTypes);
+                }
+            } else {
+                numSections += isPropertyIndexable(property) ? 1 : 0;
+            }
+        }
+        visitedSchemaTypes.remove(schemaType);
+        cachedNumSectionsInSchema.put(schemaType, numSections);
+        return numSections;
+    }
+
+    private static boolean isPropertyIndexable(PropertyConfig propertyConfig) {
+        switch (propertyConfig.getDataType()) {
+            case PropertyConfig.DATA_TYPE_STRING:
+                return ((StringPropertyConfig) propertyConfig).getIndexingType()
+                        != StringPropertyConfig.INDEXING_TYPE_NONE;
+            case PropertyConfig.DATA_TYPE_LONG:
+                return ((LongPropertyConfig) propertyConfig).getIndexingType()
+                        != LongPropertyConfig.INDEXING_TYPE_NONE;
+            case PropertyConfig.DATA_TYPE_DOCUMENT:
+                DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) propertyConfig;
+                return documentProperty.shouldIndexNestedProperties()
+                        || !documentProperty.getIndexableNestedProperties().isEmpty();
+            case PropertyConfig.DATA_TYPE_DOUBLE:
+                // fallthrough
+            case PropertyConfig.DATA_TYPE_BOOLEAN:
+                // fallthrough
+            case PropertyConfig.DATA_TYPE_BYTES:
+                // fallthrough
+            default:
+                return false;
+        }
+    }
+}
diff --git a/appsearch/appsearch-play-services-storage/build.gradle b/appsearch/appsearch-play-services-storage/build.gradle
index 4de462e..34b64a7 100644
--- a/appsearch/appsearch-play-services-storage/build.gradle
+++ b/appsearch/appsearch-play-services-storage/build.gradle
@@ -30,8 +30,7 @@
 
 dependencies {
     implementation project(":appsearch:appsearch")
-    // TODO(b/278583111) Swap with the core library version in which Function gets added.
-    implementation project(":core:core")
+    implementation("androidx.core:core:1.12.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation('androidx.collection:collection:1.2.0')
     implementation("com.google.android.gms:play-services-appsearch:16.0.0", {
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
index 2f580b9..6393a15 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -26,88 +26,68 @@
 final class FeaturesImpl implements Features {
     @Override
     public boolean isFeatureSupported(@NonNull String feature) {
-        // TODO(b/274986359): Update based on features available in {@link Features} and those
-        //  supported by play-services-appsearch.
         switch (feature) {
-            // Android T Features
             case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
                 // fall through
             case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
                 // fall through
             case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
                 // fall through
+            case Features.JOIN_SPEC_AND_QUALIFIED_ID:
+                // fall through
+            case Features.NUMERIC_SEARCH:
+                // fall through
+            case Features.VERBATIM_SEARCH:
+                // fall through
+            case Features.LIST_FILTER_QUERY_LANGUAGE:
+                // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // fall through
+            case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+                // fall through
             case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
                 // fall through
-                return true; // AppSearch features in T, present in GMSCore AppSearch.
+            case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+                // fall through
+            case Features.TOKENIZER_TYPE_RFC822:
+                // fall through
+            case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+                // fall through
+            case Features.SEARCH_SUGGESTION:
+                // fall through
+            case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
+                // fall through
+            case Features.SCHEMA_ADD_PARENT_TYPE:
+                // fall through
+            case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+                // fall through
+            case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                return true; // AppSearch features present in GMSCore AppSearch.
 
             // RegisterObserver and UnregisterObserver are not yet supported by GMSCore AppSearch.
             // TODO(b/208654892) : Update to reflect support once this feature is supported.
             case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
-            // Android U Features
-            case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
-                // TODO(b/203700301) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
                 // fall through
-            case Features.TOKENIZER_TYPE_RFC822:
-                // TODO(b/259294369) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
                 // fall through
-            case Features.NUMERIC_SEARCH:
-                // TODO(b/259744228) : Update to reflect support in Android U+ once this feature is
-                // synced over into service-appsearch.
+            case Features.SCHEMA_SET_DESCRIPTION:
                 // fall through
-            case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
-                // TODO(b/261474063) : Update to reflect support in Android U+ once advanced
-                //  ranking becomes available.
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
                 // fall through
-            case Features.JOIN_SPEC_AND_QUALIFIED_ID:
-                // TODO(b/256022027) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
                 // fall through
-            case Features.VERBATIM_SEARCH:
-                // TODO(b/204333391) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.LIST_FILTER_QUERY_LANGUAGE:
-                // TODO(b/208654892) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
-                // TODO(b/258715421) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SEARCH_SUGGESTION:
-                // TODO(b/227356108) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // TODO(b/268521214) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
-                // TODO(b/280698121) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SCHEMA_ADD_PARENT_TYPE:
-                // TODO(b/269295094) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
-                // TODO(b/289150947) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
-                // TODO(b/296088047) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                return false;
             default:
-                return false; // AppSearch features in U+, absent in GMSCore AppSearch.
+                return false; // AppSearch features absent in GMSCore AppSearch.
         }
     }
     @Override
     public int getMaxIndexedProperties() {
-        // TODO(b/241310816): Update to reflect support in Android U+ once 64 indexable properties
-        //  are possible in service-appsearch.
-        return 16;
+        return 64;
     }
 }
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
index 61c90ac..5bc02ef 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/GlobalSearchSessionImpl.java
@@ -56,6 +56,8 @@
     private final Features mFeatures;
     private final Executor mExecutor;
 
+    private boolean mIsClosed = false;
+
     GlobalSearchSessionImpl(
             @NonNull GlobalSearchClient gmsClient,
             @NonNull Features features,
@@ -69,6 +71,10 @@
     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, "GlobalSearchSession has already been closed");
         return AppSearchTaskFutures.toListenableFuture(
                 mGmsClient.getByDocumentId(packageName, databaseName,
                         RequestToGmsConverter.toGmsGetByDocumentIdRequest(request)),
@@ -80,6 +86,9 @@
     @NonNull
     @Override
     public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         com.google.android.gms.appsearch.SearchResults searchResults =
                 mGmsClient.search(queryExpression,
                         SearchSpecToGmsConverter.toGmsSearchSpec(searchSpec));
@@ -90,6 +99,8 @@
     @Override
     public ListenableFuture<Void> reportSystemUsageAsync(
             @NonNull ReportSystemUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         Task<Void> flushTask = Tasks.forResult(null);
         return AppSearchTaskFutures.toListenableFuture(flushTask, /* valueMapper= */ i-> i,
                 mExecutor);
@@ -99,6 +110,9 @@
     @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 AppSearchTaskFutures.toListenableFuture(
                 mGmsClient.getSchema(packageName, databaseName),
                 GetSchemaResponseToGmsConverter::toJetpackGetSchemaResponse, mExecutor);
@@ -130,6 +144,6 @@
 
     @Override
     public void close() {
-        mGmsClient.close();
+        mIsClosed = true;
     }
 }
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
index ca2bee2..8a3556d 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
@@ -33,7 +34,6 @@
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * An AppSearch storage system which stores data in the central AppSearch service in Google
@@ -200,7 +200,8 @@
     // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     /**
      * Opens a new {@link AppSearchSession} on this storage.
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
index df99af7..998ded9 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/SearchSessionImpl.java
@@ -40,6 +40,8 @@
 import androidx.appsearch.playservicesstorage.converter.RequestToGmsConverter;
 import androidx.appsearch.playservicesstorage.converter.ResponseToGmsConverter;
 import androidx.appsearch.playservicesstorage.converter.SearchSpecToGmsConverter;
+import androidx.appsearch.playservicesstorage.converter.SearchSuggestionResultToGmsConverter;
+import androidx.appsearch.playservicesstorage.converter.SearchSuggestionSpecToGmsConverter;
 import androidx.appsearch.playservicesstorage.converter.SetSchemaRequestToGmsConverter;
 import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
 import androidx.core.util.Preconditions;
@@ -149,9 +151,16 @@
     public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
             @NonNull String suggestionQueryExpression,
             @NonNull SearchSuggestionSpec searchSuggestionSpec) {
-        // TODO(b/274986359): Implement searchSuggestionAsync for PlayServicesStorage.
-        throw new UnsupportedOperationException(
-                "Search Suggestion is not yet supported on this AppSearch implementation.");
+        Preconditions.checkNotNull(suggestionQueryExpression);
+        Preconditions.checkNotNull(searchSuggestionSpec);
+        return AppSearchTaskFutures.toListenableFuture(
+                mGmsClient.searchSuggestion(
+                        suggestionQueryExpression,
+                        SearchSuggestionSpecToGmsConverter.toGmsSearchSuggestionSpec(
+                                searchSuggestionSpec),
+                        mDatabaseName),
+                SearchSuggestionResultToGmsConverter :: toGmsSearchSuggestionResults,
+                mExecutor);
     }
 
     @NonNull
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
index 2b78fbf..0aec84e 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
@@ -18,6 +18,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
@@ -71,6 +73,10 @@
                 }
                 gmsBuilder.setPropertyDocument(propertyName,
                         gmsSubDocuments);
+            } else if (property instanceof EmbeddingVector[]) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
             } else {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s",
@@ -121,6 +127,8 @@
                 }
                 jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
             } else {
+                // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+                //  available in gms-appsearch.
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
                                 property.getClass().toString()));
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
index bacb0b8..bb37609 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GetSchemaResponseToGmsConverter.java
@@ -20,9 +20,13 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -67,9 +71,73 @@
             jetpackBuilder.setRequiredPermissionsForSchemaTypeVisibility(entry.getKey(),
                     entry.getValue());
         }
+        // Convert publicly visible schemas
+        Map<String, PackageIdentifier> publiclyVisibleSchemas =
+                getPubliclyVisibleSchemas(gmsResponse);
+        if (!publiclyVisibleSchemas.isEmpty()) {
+            for (Map.Entry<String, PackageIdentifier> entry :
+                    publiclyVisibleSchemas.entrySet()) {
+                jetpackBuilder.setPubliclyVisibleSchema(entry.getKey(), entry.getValue());
+            }
+        }
+
+        // Convert schemas visible to configs
+        Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+                getSchemasVisibleToConfigs(gmsResponse);
+        if (!schemasVisibleToConfigs.isEmpty()) {
+            for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                    schemasVisibleToConfigs.entrySet()) {
+                jetpackBuilder.setSchemaTypeVisibleToConfigs(entry.getKey(), entry.getValue());
+            }
+        }
         return jetpackBuilder.build();
     }
 
+    private static Map<String, PackageIdentifier> getPubliclyVisibleSchemas(
+            com.google.android.gms.appsearch.GetSchemaResponse gmsResponse) {
+        Map<String, com.google.android.gms.appsearch.PackageIdentifier>
+                gmsPubliclyVisibleSchemas = gmsResponse.getPubliclyVisibleSchemas();
+        if (gmsPubliclyVisibleSchemas.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, PackageIdentifier> jetpackPubliclyVisibleSchemas =
+                new ArrayMap<>(gmsPubliclyVisibleSchemas.size());
+        for (Map.Entry<String, com.google.android.gms.appsearch.PackageIdentifier> entry :
+                gmsPubliclyVisibleSchemas.entrySet()) {
+            jetpackPubliclyVisibleSchemas.put(
+                    entry.getKey(),
+                    new PackageIdentifier(
+                            entry.getValue().getPackageName(),
+                            entry.getValue().getSha256Certificate()));
+        }
+        return jetpackPubliclyVisibleSchemas;
+    }
+
+    private static Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs(
+            com.google.android.gms.appsearch.GetSchemaResponse gmsResponse) {
+        Map<String, Set<com.google.android.gms.appsearch.SchemaVisibilityConfig>>
+                gmsSchemasVisibleToConfigs =
+                gmsResponse.getSchemaTypesVisibleToConfigs();
+        if (gmsSchemasVisibleToConfigs.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, Set<SchemaVisibilityConfig>> jetpackSchemasVisibleToConfigs =
+                new ArrayMap<>(gmsSchemasVisibleToConfigs.size());
+        for (Map.Entry<String, Set<com.google.android.gms.appsearch.SchemaVisibilityConfig>> entry :
+                gmsSchemasVisibleToConfigs.entrySet()) {
+            Set<SchemaVisibilityConfig> jetpackConfigPerType =
+                    new ArraySet<>(entry.getValue().size());
+            for (com.google.android.gms.appsearch.SchemaVisibilityConfig gmsConfigPerType :
+                    entry.getValue()) {
+                SchemaVisibilityConfig jetpackConfig =
+                        toJetpackSchemaVisibilityConfig(gmsConfigPerType);
+                jetpackConfigPerType.add(jetpackConfig);
+            }
+            jetpackSchemasVisibleToConfigs.put(entry.getKey(), jetpackConfigPerType);
+        }
+        return jetpackSchemasVisibleToConfigs;
+    }
+
     /**
      * Adds package visibilities in a Gms
      * {@link com.google.android.gms.appsearch.GetSchemaResponse} into
@@ -97,4 +165,42 @@
             }
         }
     }
+
+    /**
+     * Translates a platform {@link com.google.android.gms.appsearch.SchemaVisibilityConfig} into
+     * a jetpack
+     * {@link SchemaVisibilityConfig}.
+     */
+    @NonNull
+    private static SchemaVisibilityConfig toJetpackSchemaVisibilityConfig(
+            @NonNull com.google.android.gms.appsearch.SchemaVisibilityConfig platformConfig) {
+        Preconditions.checkNotNull(platformConfig);
+        SchemaVisibilityConfig.Builder jetpackBuilder = new SchemaVisibilityConfig.Builder();
+
+        // Translate allowedPackages
+        List<com.google.android.gms.appsearch.PackageIdentifier> allowedPackages =
+                platformConfig.getAllowedPackages();
+        for (int i = 0; i < allowedPackages.size(); i++) {
+            jetpackBuilder.addAllowedPackage(new PackageIdentifier(
+                    allowedPackages.get(i).getPackageName(),
+                    allowedPackages.get(i).getSha256Certificate()));
+        }
+
+        // Translate requiredPermissions
+        for (Set<Integer> requiredPermissions : platformConfig.getRequiredPermissions()) {
+            jetpackBuilder.addRequiredPermissions(requiredPermissions);
+        }
+
+        // Translate publiclyVisibleTargetPackage
+        com.google.android.gms.appsearch.PackageIdentifier publiclyVisibleTargetPackage =
+                platformConfig.getPubliclyVisibleTargetPackage();
+        if (publiclyVisibleTargetPackage != null) {
+            jetpackBuilder.setPubliclyVisibleTargetPackage(
+                    new PackageIdentifier(
+                            publiclyVisibleTargetPackage.getPackageName(),
+                            publiclyVisibleTargetPackage.getSha256Certificate()));
+        }
+
+        return jetpackBuilder.build();
+    }
 }
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java
new file mode 100644
index 0000000..08d661f
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/JoinSpecToGmsConverter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.playservicesstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.JoinSpec;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link JoinSpec}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class JoinSpecToGmsConverter {
+    private JoinSpecToGmsConverter() {
+    }
+
+    /**
+     * Translates a Jetpack {@link JoinSpec} into a gms
+     * {@link com.google.android.gms.appsearch.JoinSpec}.
+     */
+    @SuppressLint("WrongConstant")
+    @NonNull
+    public static com.google.android.gms.appsearch.JoinSpec toGmsJoinSpec(
+            @NonNull JoinSpec jetpackSpec) {
+        Preconditions.checkNotNull(jetpackSpec);
+        return new com.google.android.gms.appsearch.JoinSpec
+                .Builder(jetpackSpec.getChildPropertyExpression())
+                .setNestedSearch(
+                        jetpackSpec.getNestedQuery(),
+                        SearchSpecToGmsConverter.toGmsSearchSpec(
+                                jetpackSpec.getNestedSearchSpec()))
+                .setMaxJoinedResultCount(jetpackSpec.getMaxJoinedResultCount())
+                .setAggregationScoringStrategy(jetpackSpec.getAggregationScoringStrategy())
+                .build();
+    }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
index 34cebb8..8c6f88a 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
@@ -47,10 +47,18 @@
         Preconditions.checkNotNull(jetpackRequest);
         com.google.android.gms.appsearch.PutDocumentsRequest.Builder gmsBuilder =
                 new com.google.android.gms.appsearch.PutDocumentsRequest.Builder();
+        // Convert normal generic documents.
         for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
             gmsBuilder.addGenericDocuments(
+                    GenericDocumentToGmsConverter.toGmsGenericDocument(jetpackDocument));
+        }
+        // Convert taken action generic documents.
+        for (GenericDocument jetpackTakenActionGenericDocument :
+                jetpackRequest.getTakenActionGenericDocuments()) {
+            gmsBuilder.addTakenActionGenericDocuments(
                     GenericDocumentToGmsConverter.toGmsGenericDocument(
-                            jetpackDocument));
+                            jetpackTakenActionGenericDocument)
+            );
         }
         return gmsBuilder.build();
     }
@@ -68,7 +76,7 @@
                         jetpackRequest.getNamespace())
                         .addIds(jetpackRequest.getIds());
         for (Map.Entry<String, List<String>> projection :
-                jetpackRequest.getProjectionsInternal().entrySet()) {
+                jetpackRequest.getProjections().entrySet()) {
             gmsBuilder.addProjection(projection.getKey(), projection.getValue());
         }
         return gmsBuilder.build();
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
index 3c507a5..a777688 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.Features;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -44,6 +45,17 @@
         com.google.android.gms.appsearch.AppSearchSchema.Builder gmsBuilder =
                 new com.google.android.gms.appsearch.AppSearchSchema
                         .Builder(jetpackSchema.getSchemaType());
+        if (!jetpackSchema.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
+        if (!jetpackSchema.getParentTypes().isEmpty()) {
+            List<String> parentTypes = jetpackSchema.getParentTypes();
+            for (int i = 0; i < parentTypes.size(); i++) {
+                gmsBuilder.addParentType(parentTypes.get(i));
+            }
+        }
         List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig gmsProperty =
@@ -64,8 +76,14 @@
         Preconditions.checkNotNull(gmsSchema);
         AppSearchSchema.Builder jetpackBuilder =
                 new AppSearchSchema.Builder(gmsSchema.getSchemaType());
+        // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+        //  available in gms.
         List<com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig> properties =
                 gmsSchema.getProperties();
+        List<String> parentTypes = gmsSchema.getParentTypes();
+        for (int i = 0; i < parentTypes.size(); i++) {
+            jetpackBuilder.addParentType(parentTypes.get(i));
+        }
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
             jetpackBuilder.addProperty(jetpackProperty);
@@ -77,6 +95,11 @@
     private static com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig toGmsProperty(
             @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
         Preconditions.checkNotNull(jetpackProperty);
+        if (!jetpackProperty.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
         if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
             AppSearchSchema.StringPropertyConfig stringProperty =
                     (AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -87,18 +110,9 @@
                             .setCardinality(stringProperty.getCardinality())
                             .setIndexingType(stringProperty.getIndexingType())
                             .setTokenizerType(stringProperty.getTokenizerType());
-            if (stringProperty.getDeletionPropagation()) {
-                // TODO(b/268521214): Update once deletion propagation is available.
-                throw new UnsupportedOperationException("Setting deletion propagation is not "
-                        + "supported on this AppSearch implementation.");
-            }
-
             if (stringProperty.getJoinableValueType()
                     == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
-                //TODO(b/274986359) Add GMSCore feature check for Joins once available.
-                throw new UnsupportedOperationException(
-                        "StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID is not supported"
-                                + " on this AppSearch implementation.");
+                gmsBuilder.setJoinableValueType(stringProperty.getJoinableValueType());
             }
             return gmsBuilder.build();
         } else if (jetpackProperty instanceof AppSearchSchema.LongPropertyConfig) {
@@ -111,10 +125,7 @@
                             .setCardinality(jetpackProperty.getCardinality());
             if (longProperty.getIndexingType()
                     == AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE) {
-                //TODO(b/274986359) Add GMSCore feature check for Indexing Range once available.
-                throw new UnsupportedOperationException(
-                        "LongProperty.INDEXING_TYPE_RANGE is not supported on this AppSearch "
-                                + "implementation.");
+                longPropertyBuilder.setIndexingType(longProperty.getIndexingType());
             }
             return longPropertyBuilder.build();
         } else if (jetpackProperty instanceof AppSearchSchema.DoublePropertyConfig) {
@@ -139,11 +150,16 @@
             AppSearchSchema.DocumentPropertyConfig documentProperty =
                     (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
             return new com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig
-                    .Builder(
-                    documentProperty.getName(), documentProperty.getSchemaType())
+                    .Builder(documentProperty.getName(), documentProperty.getSchemaType())
                     .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
-                    .build();
+                    .setShouldIndexNestedProperties(
+                            documentProperty.shouldIndexNestedProperties())
+                    .addIndexableNestedProperties(
+                            documentProperty.getIndexableNestedProperties()).build();
+        } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            // TODO(b/326656531): Remove this once embedding search APIs are available.
+            throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                    + " is not available on this AppSearch implementation.");
         } else {
             throw new IllegalArgumentException(
                     "Invalid dataType: " + jetpackProperty.getDataType());
@@ -160,31 +176,45 @@
             com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
                     (com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig)
                             gmsProperty;
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            //  available in gms.
             return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
                     .setCardinality(stringProperty.getCardinality())
                     .setIndexingType(stringProperty.getIndexingType())
                     .setTokenizerType(stringProperty.getTokenizerType())
+                    .setJoinableValueType(stringProperty.getJoinableValueType())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig) {
+            com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig longProperty =
+                    (com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig)
+                            gmsProperty;
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            //  available in gms.
             return new AppSearchSchema.LongPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
+                    .setIndexingType(longProperty.getIndexingType())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            //  available in gms.
             return new AppSearchSchema.DoublePropertyConfig.Builder(
                     gmsProperty.getName())
-                    .setCardinality(gmsProperty.getCardinality())
-                    .build();
+                    .setCardinality(gmsProperty.getCardinality()).build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in gms.
             return new AppSearchSchema.BooleanPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in gms.
             return new AppSearchSchema.BytesPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
@@ -196,13 +226,20 @@
                     documentProperty =
                     (com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig)
                             gmsProperty;
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            //  available in gms.
             return new AppSearchSchema.DocumentPropertyConfig.Builder(
                     documentProperty.getName(),
                     documentProperty.getSchemaType())
                     .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
+                    .setShouldIndexNestedProperties(
+                            documentProperty.shouldIndexNestedProperties())
+                    .addIndexableNestedProperties(
+                            documentProperty.getIndexableNestedProperties())
                     .build();
         } else {
+            // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+            //  available in gms-appsearch.
             throw new IllegalArgumentException(
                     "Invalid property type " + gmsProperty.getClass()
                             + ": " + gmsProperty);
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
index 81a0206..91afc41 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchResultToGmsConverter.java
@@ -68,6 +68,10 @@
                     gmsMatches.get(i));
             builder.addMatchInfo(jetpackMatchInfo);
         }
+        for (com.google.android.gms.appsearch.SearchResult joinedResult :
+                gmsResult.getJoinedResults()) {
+            builder.addJoinedResult(toJetpackSearchResult(joinedResult));
+        }
         return builder.build();
     }
 
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
index 1b7701b..0d687c0 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
@@ -27,7 +27,6 @@
 
 /**
  * Translates between Gms and Jetpack versions of {@link SearchSpec}.
-
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SearchSpecToGmsConverter {
@@ -43,10 +42,7 @@
                 new com.google.android.gms.appsearch.SearchSpec.Builder();
 
         if (!jetpackSearchSpec.getAdvancedRankingExpression().isEmpty()) {
-            //TODO(b/274986359) Add GMSCore feature check for Advanced Ranking once available.
-            throw new UnsupportedOperationException(
-                    Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION
-                            + " is not available on this AppSearch implementation.");
+            gmsBuilder.setRankingStrategy(jetpackSearchSpec.getAdvancedRankingExpression());
         } else {
             gmsBuilder.setRankingStrategy(jetpackSearchSpec.getRankingStrategy());
         }
@@ -57,7 +53,6 @@
                 .addFilterNamespaces(jetpackSearchSpec.getFilterNamespaces())
                 .addFilterPackageNames(jetpackSearchSpec.getFilterPackageNames())
                 .setResultCountPerPage(jetpackSearchSpec.getResultCountPerPage())
-                .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())
                 .setOrder(jetpackSearchSpec.getOrder())
                 .setSnippetCount(jetpackSearchSpec.getSnippetCount())
                 .setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
@@ -72,36 +67,61 @@
             gmsBuilder.addProjection(projection.getKey(), projection.getValue());
         }
 
-        if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
-            if (jetpackSearchSpec.isNumericSearchEnabled()
-                    || jetpackSearchSpec.isVerbatimSearchEnabled()
-                    || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
-                //TODO(b/274986359) Add GMSCore feature check for NUMERIC_SEARCH,
-                // VERBATIM_SEARCH and LIST_FILTER_QUERY_LANGUAGE once available.
-                throw new UnsupportedOperationException(
-                        "Advanced query features (NUMERIC_SEARCH, VERBATIM_SEARCH and "
-                                + "LIST_FILTER_QUERY_LANGUAGE) are not supported with this "
-                                + "backend/Android API level combination.");
+        if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
+            for (Map.Entry<String, Map<String, Double>> entry :
+                    jetpackSearchSpec.getPropertyWeights().entrySet()) {
+                gmsBuilder.setPropertyWeights(entry.getKey(), entry.getValue());
             }
         }
 
-        if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
-            //TODO(b/274986359) Add GMSCore feature check for Property Weights once available.
-            throw new UnsupportedOperationException(
-                    "Property weights are not supported with this backend/Android API level "
-                            + "combination.");
+        if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+            if (jetpackSearchSpec.isNumericSearchEnabled()) {
+                gmsBuilder.setNumericSearchEnabled(true);
+            }
+            if (jetpackSearchSpec.isVerbatimSearchEnabled()) {
+                gmsBuilder.setVerbatimSearchEnabled(true);
+            }
+            if (jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
+                gmsBuilder.setListFilterQueryLanguageEnabled(true);
+            }
+            if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+                gmsBuilder.setListFilterHasPropertyFunctionEnabled(true);
+            }
+            // Copy beyond-V features
+            if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+                    || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
+            }
+            if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+                // TODO(b/332620561): Remove this once 'tokenize' is supported.
+                throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                        + " is not available on this AppSearch implementation.");
+            }
         }
 
         if (jetpackSearchSpec.getJoinSpec() != null) {
-            //TODO(b/274986359) Add GMSCore feature check for Joins once available.
-            throw new UnsupportedOperationException("JoinSpec is not available on this "
-                    + "AppSearch implementation.");
+            gmsBuilder.setJoinSpec(JoinSpecToGmsConverter.toGmsJoinSpec(
+                    jetpackSearchSpec.getJoinSpec()));
         }
 
         if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
-            // TODO(b/296088047): Remove this once property filters become available.
-            throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
-                    + " is not available on this AppSearch implementation.");
+            for (Map.Entry<String, List<String>> entry :
+                    jetpackSearchSpec.getFilterProperties().entrySet()) {
+                gmsBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+            }
+        }
+
+        if (jetpackSearchSpec.getSearchSourceLogTag() != null) {
+            gmsBuilder.setSearchSourceLogTag(jetpackSearchSpec.getSearchSourceLogTag());
+        }
+
+        if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+            // TODO(b/332642571): Remove this once informational ranking expressions are available.
+            throw new UnsupportedOperationException(
+                    Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                            + " are not available on this AppSearch implementation.");
         }
 
         return gmsBuilder.build();
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java
new file mode 100644
index 0000000..f5b8568
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionResultToGmsConverter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.playservicesstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link SearchSuggestionResult}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchSuggestionResultToGmsConverter {
+    private SearchSuggestionResultToGmsConverter() {}
+
+    /** Translates from Platform to Jetpack versions of {@linkSearchSuggestionResult}   */
+    @NonNull
+    public static List<SearchSuggestionResult> toGmsSearchSuggestionResults(
+            @NonNull List<com.google.android.gms.appsearch.SearchSuggestionResult>
+                    gmsSearchSuggestionResults) {
+        Preconditions.checkNotNull(gmsSearchSuggestionResults);
+        List<SearchSuggestionResult> jetpackSearchSuggestionResults =
+                new ArrayList<>(gmsSearchSuggestionResults.size());
+        for (int i = 0; i < gmsSearchSuggestionResults.size(); i++) {
+            jetpackSearchSuggestionResults.add(new SearchSuggestionResult.Builder()
+                    .setSuggestedResult(gmsSearchSuggestionResults.get(i).getSuggestedResult())
+                    .build());
+        }
+        return jetpackSearchSuggestionResults;
+    }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java
new file mode 100644
index 0000000..18556a7
--- /dev/null
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSuggestionSpecToGmsConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.playservicesstorage.converter;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Gms and Jetpack versions of {@link SearchSuggestionSpec}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SearchSuggestionSpecToGmsConverter {
+    private SearchSuggestionSpecToGmsConverter() {
+    }
+
+    /** Translates from Jetpack to Gms version of {@link SearchSuggestionSpec}. */
+    // Most jetpackSearchSuggestionSpec.get calls cause WrongConstant lint errors because the
+    // methods are not defined as returning the same constants as the corresponding setter
+    // expects, but they do
+    @SuppressLint("WrongConstant")
+    @NonNull
+    public static com.google.android.gms.appsearch.SearchSuggestionSpec toGmsSearchSuggestionSpec(
+            @NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
+        Preconditions.checkNotNull(jetpackSearchSuggestionSpec);
+
+        com.google.android.gms.appsearch.SearchSuggestionSpec.Builder gmsBuilder =
+                new com.google.android.gms.appsearch.SearchSuggestionSpec.Builder(
+                        jetpackSearchSuggestionSpec.getMaximumResultCount());
+
+        gmsBuilder
+                .addFilterNamespaces(jetpackSearchSuggestionSpec.getFilterNamespaces())
+                .addFilterSchemas(jetpackSearchSuggestionSpec.getFilterSchemas())
+                .setRankingStrategy(jetpackSearchSuggestionSpec.getRankingStrategy());
+        for (Map.Entry<String, List<String>> documentIdFilters :
+                jetpackSearchSuggestionSpec.getFilterDocumentIds().entrySet()) {
+            gmsBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
+                    documentIdFilters.getValue());
+        }
+
+        Map<String, List<String>> jetpackFilterProperties =
+                jetpackSearchSuggestionSpec.getFilterProperties();
+        if (!jetpackFilterProperties.isEmpty()) {
+            for (Map.Entry<String, List<String>> entry : jetpackFilterProperties.entrySet()) {
+                gmsBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+            }
+        }
+        return gmsBuilder.build();
+    }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
index 3929b54..348849c 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SetSchemaRequestToGmsConverter.java
@@ -22,16 +22,17 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.core.util.Preconditions;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
  * Translates between Gms and Jetpack versions of {@link SetSchemaRequest}.
-
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SetSchemaRequestToGmsConverter {
@@ -76,6 +77,28 @@
                 }
             }
         }
+        if (!jetpackRequest.getPubliclyVisibleSchemas().isEmpty()) {
+            for (Map.Entry<String, PackageIdentifier> entry :
+                    jetpackRequest.getPubliclyVisibleSchemas().entrySet()) {
+                PackageIdentifier publiclyVisibleTargetPackage = entry.getValue();
+                gmsBuilder.setPubliclyVisibleSchema(
+                        entry.getKey(),
+                        new com.google.android.gms.appsearch.PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+        }
+
+        if (!jetpackRequest.getSchemasVisibleToConfigs().isEmpty()) {
+            for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                    jetpackRequest.getSchemasVisibleToConfigs().entrySet()) {
+                for (SchemaVisibilityConfig jetpackConfig : entry.getValue()) {
+                    com.google.android.gms.appsearch.SchemaVisibilityConfig gmsConfig =
+                            toGmsSchemaVisibilityConfig(jetpackConfig);
+                    gmsBuilder.addSchemaTypeVisibleToConfig(entry.getKey(), gmsConfig);
+                }
+            }
+        }
         for (Map.Entry<String, Migrator> entry : jetpackRequest.getMigrators().entrySet()) {
             Migrator jetpackMigrator = entry.getValue();
             com.google.android.gms.appsearch.Migrator gmsMigrator =
@@ -161,4 +184,41 @@
         }
         return jetpackBuilder.build();
     }
+
+    /**
+     * Translates a jetpack {@link SchemaVisibilityConfig} into a gms
+     * {@link com.google.android.gms.appsearch.SchemaVisibilityConfig}.
+     */
+    @NonNull
+    private static com.google.android.gms.appsearch.SchemaVisibilityConfig
+            toGmsSchemaVisibilityConfig(@NonNull SchemaVisibilityConfig jetpackConfig) {
+        Preconditions.checkNotNull(jetpackConfig);
+        com.google.android.gms.appsearch.SchemaVisibilityConfig.Builder gmsBuilder =
+                new com.google.android.gms.appsearch.SchemaVisibilityConfig.Builder();
+
+        // Translate allowedPackages
+        List<PackageIdentifier> allowedPackages = jetpackConfig.getAllowedPackages();
+        for (int i = 0; i < allowedPackages.size(); i++) {
+            gmsBuilder.addAllowedPackage(new com.google.android.gms.appsearch.PackageIdentifier(
+                    allowedPackages.get(i).getPackageName(),
+                    allowedPackages.get(i).getSha256Certificate()));
+        }
+
+        // Translate requiredPermissions
+        for (Set<Integer> requiredPermissions : jetpackConfig.getRequiredPermissions()) {
+            gmsBuilder.addRequiredPermissions(requiredPermissions);
+        }
+
+        // Translate publiclyVisibleTargetPackage
+        PackageIdentifier publiclyVisibleTargetPackage =
+                jetpackConfig.getPubliclyVisibleTargetPackage();
+        if (publiclyVisibleTargetPackage != null) {
+            gmsBuilder.setPubliclyVisibleTargetPackage(
+                    new com.google.android.gms.appsearch.PackageIdentifier(
+                            publiclyVisibleTargetPackage.getPackageName(),
+                            publiclyVisibleTargetPackage.getSha256Certificate()));
+        }
+
+        return gmsBuilder.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 6d996e9..7391cbb 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,7 +27,9 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -115,14 +117,56 @@
     }
 
     /**
-     * Creates a mock {@link VisibilityChecker}.
+     * Creates a mock {@link VisibilityChecker} where schema is searchable if prefixedSchema is
+     * one of the provided set of visiblePrefixedSchemas and caller does not have system access.
+     *
      * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
-     * @return
+     * @return Mocked {@link VisibilityChecker} instance.
      */
     @NonNull
     public static VisibilityChecker createMockVisibilityChecker(
             @NonNull Set<String> visiblePrefixedSchemas) {
-        return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
-                visiblePrefixedSchemas.contains(prefixedSchema);
+        return new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(
+                    @NonNull CallerAccess callerAccess,
+                    @NonNull String packageName,
+                    @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return visiblePrefixedSchemas.contains(prefixedSchema);
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+                return false;
+            }
+        };
+    }
+
+    /**
+     * Creates a mock {@link VisibilityChecker}, where it can be configured if schema is searchable
+     * by caller and caller does not have system access.
+     *
+     * @param isSchemaSearchableByCaller Schema visibility for caller.
+     * @return Mocked {@link VisibilityChecker} instance.
+     */
+    @NonNull
+    public static VisibilityChecker createMockVisibilityChecker(
+            boolean isSchemaSearchableByCaller) {
+        return new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(
+                    @NonNull CallerAccess callerAccess,
+                    @NonNull String packageName,
+                    @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return isSchemaSearchableByCaller;
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+                return false;
+            }
+        };
     }
 }
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
index 0c9464b..cc3f74c 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/SimpleTestLogger.java
@@ -25,6 +25,7 @@
 import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.stats.PutDocumentStats;
 import androidx.appsearch.localstorage.stats.RemoveStats;
+import androidx.appsearch.localstorage.stats.SearchSessionStats;
 import androidx.appsearch.localstorage.stats.SearchStats;
 import androidx.appsearch.localstorage.stats.SetSchemaStats;
 import androidx.appsearch.stats.SchemaMigrationStats;
@@ -63,6 +64,8 @@
     /** Holds {@link androidx.appsearch.stats.SchemaMigrationStats} after logging. */
     @Nullable
     public SchemaMigrationStats mSchemaMigrationStats;
+    /** Holds {@link SearchSessionStats} after logging. */
+    @NonNull public List<SearchSessionStats> mSearchSessionsStats = new ArrayList<>();
 
     @Override
     public void logStats(@NonNull CallStats stats) {
@@ -103,4 +106,9 @@
     public void logStats(@NonNull SchemaMigrationStats stats) {
         mSchemaMigrationStats = stats;
     }
+
+    @Override
+    public void logStats(@NonNull List<SearchSessionStats> searchSessionsStats) {
+        mSearchSessionsStats.addAll(searchSessionsStats);
+    }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -35,6 +35,12 @@
     method public abstract boolean required() default false;
   }
 
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
   @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
   }
 
@@ -115,11 +121,14 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_RATE_LIMITED = 10; // 0xa
     field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+    field public static final int RESULT_TIMED_OUT = 11; // 0xb
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
   public final class AppSearchSchema {
+    method public String getDescription();
     method public java.util.List<java.lang.String!> getParentTypes();
     method public java.util.List<androidx.appsearch.app.AppSearchSchema.PropertyConfig!> getProperties();
     method public String getSchemaType();
@@ -132,6 +141,7 @@
     ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
     ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
   }
 
@@ -174,6 +187,21 @@
     ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    field public static final int INDEXING_TYPE_NONE = 0; // 0x0
+    field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+    ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
   }
 
   public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
     ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
   }
 
   public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
+    method public String getDescription();
     method public String getName();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
   }
 
   public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
-    method public boolean getDeletionPropagation();
     method public int getIndexingType();
     method public int getJoinableValueType();
     method public int getTokenizerType();
@@ -217,7 +246,7 @@
     ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+    ctor public EmbeddingVector(float[], String);
+    method public String getModelSignature();
+    method public float[] getValues();
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+    method 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 public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
   public interface Features {
     method public int getMaxIndexedProperties();
     method public boolean isFeatureSupported(String);
     field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
     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 JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+    field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
     field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
     field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
-    field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+    field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+    field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+    field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
     field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
     field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
     field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+    field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
     field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
     field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+    field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+    field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
     field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
   }
@@ -287,6 +338,8 @@
     method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
     method public double getPropertyDouble(String);
     method public double[]? getPropertyDoubleArray(String);
+    method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+    method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
     method public long getPropertyLong(String);
     method public long[]? getPropertyLongArray(String);
     method public java.util.Set<java.lang.String!> getPropertyNames();
@@ -301,6 +354,7 @@
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
+    ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
     ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
     method public BuilderType setPropertyDouble(String, double...);
+    method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
     method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
   }
 
   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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
     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.SchemaVisibilityConfig!>!> getSchemaTypesVisibleToConfigs();
     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();
@@ -348,7 +405,9 @@
     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 setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
     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 setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
     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);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
 
   public final class PutDocumentsRequest {
     method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getTakenActionGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument!>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection<? extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
@@ -472,11 +534,28 @@
     method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
+  public final class SchemaVisibilityConfig {
+    method public java.util.List<androidx.appsearch.app.PackageIdentifier!> getAllowedPackages();
+    method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+    method public java.util.Set<java.util.Set<java.lang.Integer!>!> getRequiredPermissions();
+  }
+
+  public static final class SchemaVisibilityConfig.Builder {
+    ctor public SchemaVisibilityConfig.Builder();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set<java.lang.Integer!>);
+    method public androidx.appsearch.app.SchemaVisibilityConfig build();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+  }
+
   public final class SearchResult {
     method public String getDatabaseName();
     method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public <T> T getDocument(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List<java.lang.Double!> getInformationalRankingSignals();
     method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
@@ -485,6 +564,7 @@
 
   public static final class SearchResult.Builder {
     ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
     method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
     method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
     method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
 
   public final class SearchSpec {
     method public String getAdvancedRankingExpression();
+    method public int getDefaultEmbeddingSearchMetricType();
     method public java.util.List<java.lang.String!> getFilterNamespaces();
     method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
     method public java.util.List<java.lang.String!> getFilterSchemas();
+    method public java.util.List<java.lang.String!> getInformationalRankingExpressions();
     method public androidx.appsearch.app.JoinSpec? getJoinSpec();
     method public int getMaxSnippetSize();
     method public int getOrder();
@@ -540,18 +623,26 @@
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
     method public int getResultGroupingTypeFlags();
+    method public java.util.List<androidx.appsearch.app.EmbeddingVector!> getSearchEmbeddings();
+    method public String? getSearchSourceLogTag();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    method public boolean isEmbeddingSearchEnabled();
+    method public boolean isListFilterHasPropertyFunctionEnabled();
     method public boolean isListFilterQueryLanguageEnabled();
+    method public boolean isListFilterTokenizeFunctionEnabled();
     method public boolean isNumericSearchEnabled();
     method public boolean isVerbatimSearchEnabled();
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3
     field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
     field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
-    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
     field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
     field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
     field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+    field public static final String SCHEMA_TYPE_WILDCARD = "*";
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -574,15 +666,27 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
     method public androidx.appsearch.app.SearchSpec build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
   public final class SearchSuggestionSpec {
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterDocumentIds();
     method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
     method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaximumResultCount();
     method public int getRankingStrategy();
@@ -629,6 +735,10 @@
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
 
   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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
     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.SchemaVisibilityConfig!>!> getSchemasVisibleToConfigs();
     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();
@@ -653,26 +765,32 @@
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) 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<? extends java.lang.Object!>, 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
     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.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) 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 clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class<? extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
   }
 
-  public class SetSchemaResponse {
+  public final class SetSchemaResponse {
     method public java.util.Set<java.lang.String!> getDeletedTypes();
     method public java.util.Set<java.lang.String!> getIncompatibleTypes();
     method public java.util.Set<java.lang.String!> getMigratedTypes();
@@ -700,7 +818,7 @@
     method public String getSchemaType();
   }
 
-  public class StorageInfo {
+  public final class StorageInfo {
     method public int getAliveDocumentsCount();
     method public int getAliveNamespacesCount();
     method public long getSizeBytes();
@@ -771,6 +889,51 @@
 
 }
 
+package androidx.appsearch.usagereporting {
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public String? getQuery();
+    method public String? getReferencedQualifiedId();
+    method public int getResultRankGlobal();
+    method public int getResultRankInBlock();
+    method public long getTimeStayOnResultMillis();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+    ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+    ctor public ClickAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.ClickAction build();
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public int getFetchedResultCount();
+    method public String? getQuery();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+    ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+    ctor public SearchAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.SearchAction build();
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+    method public long getActionTimestampMillis();
+    method public long getDocumentTtlMillis();
+    method public String getId();
+    method public String getNamespace();
+  }
+
+}
+
 package androidx.appsearch.util {
 
   public class DocumentIdUtil {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -35,6 +35,12 @@
     method public abstract boolean required() default false;
   }
 
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
   @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
   }
 
@@ -115,11 +121,14 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_RATE_LIMITED = 10; // 0xa
     field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+    field public static final int RESULT_TIMED_OUT = 11; // 0xb
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
   public final class AppSearchSchema {
+    method public String getDescription();
     method public java.util.List<java.lang.String!> getParentTypes();
     method public java.util.List<androidx.appsearch.app.AppSearchSchema.PropertyConfig!> getProperties();
     method public String getSchemaType();
@@ -132,6 +141,7 @@
     ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
     ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
   }
 
@@ -174,6 +187,21 @@
     ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    field public static final int INDEXING_TYPE_NONE = 0; // 0x0
+    field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+    ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
   }
 
   public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
     ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
   }
 
   public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
+    method public String getDescription();
     method public String getName();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
   }
 
   public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
-    method public boolean getDeletionPropagation();
     method public int getIndexingType();
     method public int getJoinableValueType();
     method public int getTokenizerType();
@@ -217,7 +246,7 @@
     ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+    ctor public EmbeddingVector(float[], String);
+    method public String getModelSignature();
+    method public float[] getValues();
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+    method 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 public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchemaAsync(String, String);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
   public interface Features {
     method public int getMaxIndexedProperties();
     method public boolean isFeatureSupported(String);
     field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
     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 JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+    field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
     field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
     field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
-    field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+    field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+    field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+    field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
     field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
     field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
     field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+    field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
     field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
     field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+    field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+    field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
     field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
   }
@@ -287,6 +338,8 @@
     method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
     method public double getPropertyDouble(String);
     method public double[]? getPropertyDoubleArray(String);
+    method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+    method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
     method public long getPropertyLong(String);
     method public long[]? getPropertyLongArray(String);
     method public java.util.Set<java.lang.String!> getPropertyNames();
@@ -301,6 +354,7 @@
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
+    ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
     ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
     method public BuilderType setPropertyDouble(String, double...);
+    method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
     method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
   }
 
   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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
     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.SchemaVisibilityConfig!>!> getSchemaTypesVisibleToConfigs();
     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();
@@ -348,7 +405,9 @@
     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 setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
     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 setSchemaTypeVisibleToConfigs(String, java.util.Set<androidx.appsearch.app.SchemaVisibilityConfig!>);
     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);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
 
   public final class PutDocumentsRequest {
     method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getTakenActionGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<? extends java.lang.Object!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument!>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection<? extends androidx.appsearch.usagereporting.TakenAction!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
@@ -472,11 +534,28 @@
     method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
+  public final class SchemaVisibilityConfig {
+    method public java.util.List<androidx.appsearch.app.PackageIdentifier!> getAllowedPackages();
+    method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+    method public java.util.Set<java.util.Set<java.lang.Integer!>!> getRequiredPermissions();
+  }
+
+  public static final class SchemaVisibilityConfig.Builder {
+    ctor public SchemaVisibilityConfig.Builder();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set<java.lang.Integer!>);
+    method public androidx.appsearch.app.SchemaVisibilityConfig build();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+  }
+
   public final class SearchResult {
     method public String getDatabaseName();
     method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public <T> T getDocument(Class<T!>, java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!>?) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List<java.lang.Double!> getInformationalRankingSignals();
     method public java.util.List<androidx.appsearch.app.SearchResult!> getJoinedResults();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatchInfos();
     method public String getPackageName();
@@ -485,6 +564,7 @@
 
   public static final class SearchResult.Builder {
     ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
     method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
     method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
     method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
 
   public final class SearchSpec {
     method public String getAdvancedRankingExpression();
+    method public int getDefaultEmbeddingSearchMetricType();
     method public java.util.List<java.lang.String!> getFilterNamespaces();
     method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
     method public java.util.List<java.lang.String!> getFilterSchemas();
+    method public java.util.List<java.lang.String!> getInformationalRankingExpressions();
     method public androidx.appsearch.app.JoinSpec? getJoinSpec();
     method public int getMaxSnippetSize();
     method public int getOrder();
@@ -540,18 +623,26 @@
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
     method public int getResultGroupingTypeFlags();
+    method public java.util.List<androidx.appsearch.app.EmbeddingVector!> getSearchEmbeddings();
+    method public String? getSearchSourceLogTag();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    method public boolean isEmbeddingSearchEnabled();
+    method public boolean isListFilterHasPropertyFunctionEnabled();
     method public boolean isListFilterQueryLanguageEnabled();
+    method public boolean isListFilterTokenizeFunctionEnabled();
     method public boolean isNumericSearchEnabled();
     method public boolean isVerbatimSearchEnabled();
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3
     field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
     field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
-    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
     field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
     field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
     field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+    field public static final String SCHEMA_TYPE_WILDCARD = "*";
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -574,15 +666,27 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection<androidx.appsearch.app.EmbeddingVector!>);
     method public androidx.appsearch.app.SearchSpec build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
   public final class SearchSuggestionSpec {
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterDocumentIds();
     method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getFilterProperties();
     method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaximumResultCount();
     method public int getRankingStrategy();
@@ -629,6 +735,10 @@
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class<? extends java.lang.Object!>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection<java.lang.String!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class<? extends java.lang.Object!>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
 
   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!,androidx.appsearch.app.PackageIdentifier!> getPubliclyVisibleSchemas();
     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.SchemaVisibilityConfig!>!> getSchemasVisibleToConfigs();
     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();
@@ -653,26 +765,32 @@
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class<? extends java.lang.Object!>, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<? extends java.lang.Object!>!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<? extends java.lang.Object!>!>) 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<? extends java.lang.Object!>, 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
     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.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class<? extends java.lang.Object!>) 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 clearRequiredPermissionsForDocumentClassVisibility(Class<? extends java.lang.Object!>) 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<? extends java.lang.Object!>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<? extends java.lang.Object!>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class<? extends java.lang.Object!>, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
   }
 
-  public class SetSchemaResponse {
+  public final class SetSchemaResponse {
     method public java.util.Set<java.lang.String!> getDeletedTypes();
     method public java.util.Set<java.lang.String!> getIncompatibleTypes();
     method public java.util.Set<java.lang.String!> getMigratedTypes();
@@ -700,7 +818,7 @@
     method public String getSchemaType();
   }
 
-  public class StorageInfo {
+  public final class StorageInfo {
     method public int getAliveDocumentsCount();
     method public int getAliveNamespacesCount();
     method public long getSizeBytes();
@@ -771,6 +889,51 @@
 
 }
 
+package androidx.appsearch.usagereporting {
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public String? getQuery();
+    method public String? getReferencedQualifiedId();
+    method public int getResultRankGlobal();
+    method public int getResultRankInBlock();
+    method public long getTimeStayOnResultMillis();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+    ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+    ctor public ClickAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.ClickAction build();
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public int getFetchedResultCount();
+    method public String? getQuery();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+    ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+    ctor public SearchAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.SearchAction build();
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+    method public long getActionTimestampMillis();
+    method public long getDocumentTtlMillis();
+    method public String getId();
+    method public String getNamespace();
+  }
+
+}
+
 package androidx.appsearch.util {
 
   public class DocumentIdUtil {
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 8d67baa..6e8ddd6 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -44,6 +44,8 @@
     implementation('androidx.concurrent:concurrent-futures:1.0.0')
     implementation("androidx.core:core:1.6.0")
 
+    annotationProcessor project(':appsearch:appsearch-compiler')
+
     androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
     androidTestImplementation project(':appsearch:appsearch-builtin-types')
     androidTestImplementation project(':appsearch:appsearch-local-storage')
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 5d52268..7d18f74 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -24,6 +24,7 @@
 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
 import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
+import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -2764,4 +2765,160 @@
         assertThat(person.getFirstName()).isEqualTo("first");
         assertThat(person.getLastName()).isEqualTo("last");
     }
+
+    @Document
+    static class EmailWithEmbedding {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
+
+        @Document.StringProperty
+        String mSender;
+
+        // Default non-indexable embedding
+        @Document.EmbeddingProperty
+        EmbeddingVector mSenderEmbedding;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        EmbeddingVector mTitleEmbedding;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        Collection<EmbeddingVector> mReceiverEmbeddings;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        EmbeddingVector[] mBodyEmbeddings;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            EmailWithEmbedding email = (EmailWithEmbedding) o;
+            return Objects.equals(mNamespace, email.mNamespace) && Objects.equals(mId,
+                    email.mId) && Objects.equals(mSender, email.mSender)
+                    && Objects.equals(mSenderEmbedding, email.mSenderEmbedding)
+                    && Objects.equals(mTitleEmbedding, email.mTitleEmbedding)
+                    && Objects.equals(mReceiverEmbeddings, email.mReceiverEmbeddings)
+                    && Arrays.equals(mBodyEmbeddings, email.mBodyEmbeddings);
+        }
+
+        public static EmailWithEmbedding createSampleDoc() {
+            EmbeddingVector embedding1 =
+                    new EmbeddingVector(new float[]{1, 2, 3}, "model1");
+            EmbeddingVector embedding2 =
+                    new EmbeddingVector(new float[]{-1, -2, -3}, "model2");
+            EmbeddingVector embedding3 =
+                    new EmbeddingVector(new float[]{0.1f, 0.2f, 0.3f, 0.4f}, "model3");
+            EmbeddingVector embedding4 =
+                    new EmbeddingVector(new float[]{-0.1f, -0.2f, -0.3f, -0.4f}, "model3");
+            EmailWithEmbedding email = new EmailWithEmbedding();
+            email.mNamespace = "namespace";
+            email.mId = "id";
+            email.mCreationTimestampMillis = 1000;
+            email.mSender = "sender";
+            email.mSenderEmbedding = embedding1;
+            email.mTitleEmbedding = embedding2;
+            email.mReceiverEmbeddings = Collections.singletonList(embedding3);
+            email.mBodyEmbeddings = new EmbeddingVector[]{embedding3, embedding4};
+            return email;
+        }
+    }
+
+    @Test
+    public void testEmbeddingGenericDocumentConversion() throws Exception {
+        EmailWithEmbedding inEmail = EmailWithEmbedding.createSampleDoc();
+        GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inEmail);
+        GenericDocument genericDocument2 = GenericDocument.fromDocumentClass(inEmail);
+        EmailWithEmbedding outEmail = genericDocument2.toDocumentClass(EmailWithEmbedding.class);
+
+        assertThat(inEmail).isNotSameInstanceAs(outEmail);
+        assertThat(inEmail).isEqualTo(outEmail);
+        assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2);
+        assertThat(genericDocument1).isEqualTo(genericDocument2);
+    }
+
+    @Test
+    public void testEmbeddingSearch() throws Exception {
+        assumeTrue(mSession.getFeatures().isFeatureSupported(
+                Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailWithEmbedding.class)
+                .build()).get();
+
+        // Create and add a document
+        EmailWithEmbedding email = EmailWithEmbedding.createSampleDoc();
+        checkIsBatchResultSuccess(mSession.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addDocuments(email)
+                        .build()));
+
+        // An empty query should retrieve this document.
+        SearchResults searchResults = mSession.search("",
+                new SearchSpec.Builder().build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        EmailWithEmbedding outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+
+        // senderEmbedding is non-indexable, so querying for it will return nothing.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        .addSearchEmbeddings(email.mSenderEmbedding)
+                        .setRankingStrategy(
+                                "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).isEmpty();
+
+        // titleEmbedding is indexable, and querying for it using itself will return a cosine
+        // similarity score of 1.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        .addSearchEmbeddings(email.mTitleEmbedding)
+                        .setRankingStrategy(
+                                "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(1);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+
+        // Both receiverEmbeddings and bodyEmbeddings are indexable, and in this specific
+        // document, they together hold three embedding vectors with the same signature.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        // Using one of the three vectors to query
+                        .addSearchEmbeddings(email.mBodyEmbeddings[0])
+                        .setRankingStrategy(
+                                // We should get a score of 3 for "len", since there are three
+                                // embedding vectors matched.
+                                "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java
deleted file mode 100644
index 686c44f..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appsearch.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.appsearch.testutil.AppSearchEmail;
-
-import org.junit.Test;
-
-import java.util.List;
-
-/** Tests for private APIs of {@link AppSearchSchema}. */
-public class AppSearchSchemaInternalTest {
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testParentTypes() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("EmailMessage")
-                        .addParentType("Email")
-                        .addParentType("Message")
-                        .build();
-        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDuplicateParentTypes() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("EmailMessage")
-                        .addParentType("Email")
-                        .addParentType("Message")
-                        .addParentType("Email")
-                        .build();
-        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop2", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedPropertyPaths(
-                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedPropertyPaths(
-                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
-                        .addIndexableNestedProperties("prop1")
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {
-        AppSearchSchema.DocumentPropertyConfig.Builder builder =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedProperties("prop1");
-        AppSearchSchema.DocumentPropertyConfig config1 = builder.build();
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
-        builder.addIndexableNestedProperties("prop2");
-        AppSearchSchema.DocumentPropertyConfig config2 = builder.build();
-        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
-        builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));
-        AppSearchSchema.DocumentPropertyConfig config3 = builder.build();
-        assertThat(config3.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop2", "prop3");
-        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testPropertyConfig() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("Test")
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("string")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.LongPropertyConfig.Builder("long")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setIndexingType(
-                                                AppSearchSchema.LongPropertyConfig
-                                                        .INDEXING_TYPE_NONE)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setIndexingType(
-                                                AppSearchSchema.LongPropertyConfig
-                                                        .INDEXING_TYPE_RANGE)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DoublePropertyConfig.Builder("double")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                                "document1", AppSearchEmail.SCHEMA_TYPE)
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .setShouldIndexNestedProperties(true)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                                "document2", AppSearchEmail.SCHEMA_TYPE)
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .setShouldIndexNestedProperties(false)
-                                        .addIndexableNestedProperties("path1", "path2", "path3")
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setJoinableValueType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setJoinableValueType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .build())
-                        .build();
-
-        assertThat(schema.getSchemaType()).isEqualTo("Test");
-        List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
-        assertThat(properties).hasSize(10);
-
-        assertThat(properties.get(0).getName()).isEqualTo("string");
-        assertThat(properties.get(0).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
-        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
-
-        assertThat(properties.get(1).getName()).isEqualTo("long");
-        assertThat(properties.get(1).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
-                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);
-
-        assertThat(properties.get(2).getName()).isEqualTo("indexableLong");
-        assertThat(properties.get(2).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())
-                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
-
-        assertThat(properties.get(3).getName()).isEqualTo("double");
-        assertThat(properties.get(3).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
-
-        assertThat(properties.get(4).getName()).isEqualTo("boolean");
-        assertThat(properties.get(4).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
-
-        assertThat(properties.get(5).getName()).isEqualTo("bytes");
-        assertThat(properties.get(5).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
-
-        assertThat(properties.get(6).getName()).isEqualTo("document1");
-        assertThat(properties.get(6).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())
-                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))
-                                .shouldIndexNestedProperties())
-                .isEqualTo(true);
-
-        assertThat(properties.get(7).getName()).isEqualTo("document2");
-        assertThat(properties.get(7).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())
-                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
-                                .shouldIndexNestedProperties())
-                .isEqualTo(false);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
-                                .getIndexableNestedProperties())
-                .containsExactly("path1", "path2", "path3");
-
-        assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");
-        assertThat(properties.get(8).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(
-                        ((AppSearchSchema.StringPropertyConfig) properties.get(8))
-                                .getJoinableValueType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-
-        assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");
-        assertThat(properties.get(9).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(
-                        ((AppSearchSchema.StringPropertyConfig) properties.get(9))
-                                .getJoinableValueType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index c984f92..b72cf54 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -21,16 +21,12 @@
 
 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 androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.testutil.AppSearchEmail;
-import androidx.appsearch.util.DocumentIdUtil;
-import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -46,6 +42,8 @@
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
+
+/** This class holds all tests that won't be exported to the framework.  */
 public abstract class AppSearchSessionInternalTestBase {
 
     static final String DB_NAME_1 = "";
@@ -77,730 +75,10 @@
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
-    // TODO(b/228240987) delete this test when we support property restrict for multiple terms
-    @Test
-    public void testSearchSuggestion_propertyFilter() throws Exception {
-        // Schema registration
-        AppSearchSchema schemaType1 =
-                new AppSearchSchema.Builder("Type1")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertyone")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertytwo")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema schemaType2 =
-                new AppSearchSchema.Builder("Type2")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertythree")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertyfour")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
-                .get();
-
-        // Index documents
-        GenericDocument doc1 =
-                new GenericDocument.Builder<>("namespace", "id1", "Type1")
-                        .setPropertyString("propertyone", "termone")
-                        .setPropertyString("propertytwo", "termtwo")
-                        .build();
-        GenericDocument doc2 =
-                new GenericDocument.Builder<>("namespace", "id2", "Type2")
-                        .setPropertyString("propertythree", "termthree")
-                        .setPropertyString("propertyfour", "termfour")
-                        .build();
-
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
-
-        SearchSuggestionResult resultOne =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
-        SearchSuggestionResult resultTwo =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
-        SearchSuggestionResult resultThree =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
-        SearchSuggestionResult resultFour =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
-
-        // Only search for type1/propertyone
-        List<SearchSuggestionResult> suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne);
-
-        // Only search for type1/propertyone and type1/propertytwo
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1")
-                                        .addFilterProperties(
-                                                "Type1",
-                                                ImmutableList.of("propertyone", "propertytwo"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultTwo);
-
-        // Only search for type1/propertyone and type2/propertythree
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1", "Type2")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .addFilterProperties(
-                                                "Type2", ImmutableList.of("propertythree"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultThree);
-
-        // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1", "Type2")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .addFilterPropertyPaths(
-                                                "Type2",
-                                                ImmutableList.of(new PropertyPath("propertyfour")))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultFour);
-
-        // Only search for type1/propertyone and everything in type2
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFilters() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email1, email2).build()));
-
-        // Query with type property filters {"Email", ["subject", "to"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // Only email2 should be returned because email1 doesn't have the term "body" in subject
-        // or to fields
-        assertThat(documents).containsExactly(email2);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
-        // schema has body in its property filter but Email schema doesn't.
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .addFilterProperties("Note", ImmutableList.of("body"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // Only the note document should be returned because the email property filter doesn't
-        // allow searching in the body.
-        assertThat(documents).containsExactly(note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"*": ["subject", "title"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
-                        ImmutableList.of("subject", "title"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // The wildcard property filter will apply to both the Email and Note schema. The email
-        // document should be returned since it has the term "body" in its subject property. The
-        // note document should not be returned since it doesn't have the term "body" in the title
-        // property (subject property is not applicable for Note schema)
-        assertThat(documents).containsExactly(email);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
-                        ImmutableList.of("subject", "title"))
-                .addFilterProperties("Note", ImmutableList.of("body"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // The wildcard property filter will only apply to the Email schema since Note schema has
-        // its own explicit property filter specified. The email document should be returned since
-        // it has the term "body" in its subject property. The note document should also be returned
-        // since it has the term "body" in the body property.
-        assertThat(documents).containsExactly(email, note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"NonExistentType": ["to", "title"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // The supplied property filters don't apply to either schema types. Both the documents
-        // should be returned since the term "body" is present in at least one of their properties.
-        assertThat(documents).containsExactly(email, note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersEmpty() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"email": []}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
-                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        // The email document should not be returned since the property filter doesn't allow
-        // searching any property.
-        assertThat(documents).containsExactly(note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        assumeTrue(mDb1.getFeatures()
-                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
-        // A full example of how join might be used with property filters in join spec
-        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
-                .addProperty(new StringPropertyConfig.Builder("entityId")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setJoinableValueType(StringPropertyConfig
-                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("note")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("viewType")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
-                        .build()).get();
-
-        // Index 2 email documents
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        // Index 2 viewAction documents, one for email1 and the other for email2
-        String qualifiedId1 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id1");
-        String qualifiedId2 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id2");
-        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
-                .setPropertyString("entityId", qualifiedId1)
-                .setPropertyString("note", "Viewed email on Monday")
-                .setPropertyString("viewType", "Stared").build();
-        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
-                .setPropertyString("entityId", qualifiedId2)
-                .setPropertyString("note", "Viewed email on Tuesday")
-                .setPropertyString("viewType", "Viewed").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
-                                viewAction1, viewAction2)
-                        .build()));
-
-        // The nested search spec only allows searching the viewType property for viewAction
-        // schema type. It also specifies a property filter for Email schema.
-        SearchSpec nestedSearchSpec =
-                new SearchSpec.Builder()
-                        .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
-                        .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
-                                ImmutableList.of("subject"))
-                        .build();
-
-        // Search for the term "Viewed" in join spec
-        JoinSpec js = new JoinSpec.Builder("entityId")
-                .setNestedSearch("Viewed", nestedSearchSpec)
-                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
-                .build();
-
-        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
-                .setJoinSpec(js)
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-
-        List<SearchResult> sr = searchResults.getNextPageAsync().get();
-
-        // Both email docs are returned, email2 comes first because it has higher number of
-        // joined documents. The property filters for Email schema specified in the nested search
-        // specs don't apply to the outer query (otherwise none of the email documents would have
-        // been returned).
-        assertThat(sr).hasSize(2);
-
-        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
-        // the join spec, so it should be present in the joined results.
-        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
-        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
-        // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
-        // in the join spec, so it should not be present in the joined results.
-        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
-        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
-        assertThat(sr.get(1).getJoinedResults()).isEmpty();
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        assumeTrue(mDb1.getFeatures()
-                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
-        // A full example of how join might be used with property filters in join spec
-        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
-                .addProperty(new StringPropertyConfig.Builder("entityId")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setJoinableValueType(StringPropertyConfig
-                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("note")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("viewType")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
-                        .build()).get();
-
-        // Index 2 email documents
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        // Index 2 viewAction documents, one for email1 and the other for email2
-        String qualifiedId1 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id1");
-        String qualifiedId2 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id2");
-        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
-                .setPropertyString("entityId", qualifiedId1)
-                .setPropertyString("note", "Viewed email on Monday")
-                .setPropertyString("viewType", "Stared").build();
-        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
-                .setPropertyString("entityId", qualifiedId2)
-                .setPropertyString("note", "Viewed email on Tuesday")
-                .setPropertyString("viewType", "Viewed").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
-                                viewAction1, viewAction2)
-                        .build()));
-
-        // The nested search spec doesn't specify any property filters.
-        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
-
-        // Search for the term "Viewed" in join spec
-        JoinSpec js = new JoinSpec.Builder("entityId")
-                .setNestedSearch("Viewed", nestedSearchSpec)
-                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
-                .build();
-
-        // Outer search spec adds property filters for both Email and ViewAction schema
-        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
-                .setJoinSpec(js)
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
-                .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
-                .build());
-
-        List<SearchResult> sr = searchResults.getNextPageAsync().get();
-
-        // Both email docs are returned as they both satisfy the property filters for Email, email2
-        // comes first because it has higher id lexicographically.
-        assertThat(sr).hasSize(2);
-
-        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
-        // the outer spec (although those property filters are irrelevant for joined documents),
-        // it should be present in the joined results.
-        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
-        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
-        // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
-        // in the outer spec, but property filters in the outer spec should not apply on joined
-        // documents, so viewAction1 should be present in the joined results.
-        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
-        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersNotSupported() throws Exception {
-        assumeFalse(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
-        // exception is thrown
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .build();
-        UnsupportedOperationException exception =
-                assertThrows(UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec));
-        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
-                + " is not available on this AppSearch implementation.");
-    }
-
     // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
     @Test
     public void testGetSchema_joinableValueType() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
         AppSearchSchema inSchema =
                 new AppSearchSchema.Builder("Test")
                         .addProperty(
@@ -820,11 +98,6 @@
                                         .setJoinableValueType(
                                                 StringPropertyConfig
                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        // TODO(b/274157614): Export this to framework when we
-                                        //  can access hidden APIs.
-                                        // @exportToFramework:startStrip()
-                                        .setDeletionPropagation(true)
-                                        // @exportToFramework:endStrip()
                                         .build())
                         .build();
 
@@ -837,465 +110,6 @@
         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
     }
 
-    // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
-    @Test
-    public void testGetSchema_deletionPropagation_unsupported() {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-        assumeFalse(
-                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("Test")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("qualifiedIdDeletionPropagation")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setJoinableValueType(
-                                                StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .setDeletionPropagation(true)
-                                        .build())
-                        .build();
-        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();
-        Exception e =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.setSchemaAsync(request).get());
-        assertThat(e.getMessage())
-                .isEqualTo(
-                        "Setting deletion propagation is not supported "
-                                + "on this AppSearch implementation.");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
-        assumeTrue(
-                mDb1.getFeatures()
-                        .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
-        // Schema registration
-        AppSearchSchema genericSchema =
-                new AppSearchSchema.Builder("Generic")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("foo")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(AppSearchEmail.SCHEMA)
-                                .addSchemas(genericSchema)
-                                .build())
-                .get();
-
-        // Index four documents.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("namespace1", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace1", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
-        AppSearchEmail inEmail3 =
-                new AppSearchEmail.Builder("namespace2", "id3")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
-        AppSearchEmail inEmail4 =
-                new AppSearchEmail.Builder("namespace2", "id4")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
-        AppSearchEmail inEmail5 =
-                new AppSearchEmail.Builder("namespace2", "id5")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
-        GenericDocument inDoc1 =
-                new GenericDocument.Builder<>("namespace3", "id6", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
-        GenericDocument inDoc2 =
-                new GenericDocument.Builder<>("namespace3", "id7", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
-        GenericDocument inDoc3 =
-                new GenericDocument.Builder<>("namespace4", "id8", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
-
-        // Query with per package result grouping. Only the last document 'doc3' should be
-        // returned.
-        SearchResults searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* resultLimit= */ 1)
-                                .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3);
-
-        // Query with per namespace result grouping. Only the last document in each namespace should
-        // be returned ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace result grouping. Two of the last documents in each namespace
-        // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per schema result grouping. Only the last document of each schema type should
-        // be returned ('doc3', 'email5')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inEmail5);
-
-        // Query with per schema result grouping. Only the last two documents of each schema type
-        // should be returned ('doc3', 'doc2', 'email5', 'email4')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
-        // Query with per package and per namespace result grouping. Only the last document in each
-        // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per package and per namespace result grouping. Only the last two documents
-        // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
-        // 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per package and per schema type result grouping. Only the last document in
-        // each schema type should be returned. ('doc3', 'email5')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inEmail5);
-
-        // Query with per package and per schema type result grouping. Only the last two document in
-        // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
-        // Query with per namespace and per schema type result grouping. Only the last document in
-        // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace and per schema type result grouping. Only the last two documents
-        // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
-        // 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per namespace, per package and per schema type result grouping. Only the last
-        // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace, per package and per schema type result grouping. Only the last
-        // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
-        // 'email4', 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
-        assumeFalse(
-                mDb1.getFeatures()
-                        .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index four documents.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("namespace1", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace1", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
-        AppSearchEmail inEmail3 =
-                new AppSearchEmail.Builder("namespace2", "id3")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
-        AppSearchEmail inEmail4 =
-                new AppSearchEmail.Builder("namespace2", "id4")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec1 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
-                        .build();
-        UnsupportedOperationException exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec1));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec2 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_PACKAGE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec2));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec3 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec3));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec4 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec4));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-    }
-
     @Test
     public void testQuery_typeFilterWithPolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
@@ -1496,6 +310,113 @@
     }
 
     @Test
+    public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
+        // a schema filter for the "Person".
+        // This will be expanded to paths {"Person", ["name"]} and
+        // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
+        // polymorphism.
+        SearchResults searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterSchemas("Person")
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setParentTypes(Collections.singletonList("Person"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
+
+    @Test
     public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
 
@@ -1664,4 +585,167 @@
         assertThat(documents).hasSize(4);
         assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
     }
+
+    // TODO(b/336277840): Move this if setParentTypes becomes public
+    @Test
+    public void testQuery_wildcardProjection_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("sender")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+        // Index two child documents
+        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .setPropertyString("content", "Some note")
+                .build();
+        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .setPropertyString("content", "Some note")
+                .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(email, text).build()));
+
+        SearchResults searchResults = mDb1.search("Some", new SearchSpec.Builder()
+                .addFilterSchemas("Message")
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("content"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setParentTypes(Collections.singletonList("Message"))
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .build();
+        GenericDocument expectedEmail = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setParentTypes(Collections.singletonList("Message"))
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("sender", "Some sender")
+                .build();
+        assertThat(documents).containsExactly(expectedText, expectedEmail);
+    }
+
+    // TODO(b/336277840): Move this if setParentTypes becomes public
+    @Test
+    public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema = new AppSearchSchema.Builder("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema textSchema = new AppSearchSchema.Builder("Text")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("carrier")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addParentType("Message")
+                .addProperty(new StringPropertyConfig.Builder("content")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("attachment")
+                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(messageSchema, textSchema, emailSchema).build()).get();
+
+        // Index two child documents
+        GenericDocument text = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("carrier", "Network Inc")
+                .build();
+        GenericDocument email = new GenericDocument.Builder<>("namespace", "id2", "Email")
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("attachment", "Network report")
+                .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(email, text).build()));
+
+        // Both email and text would match for "Network", but only text should match as it is in the
+        // right property
+        SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
+                .addFilterSchemas("Message")
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText = new GenericDocument.Builder<>("namespace", "id1", "Text")
+                .setParentTypes(Collections.singletonList("Message"))
+                .setCreationTimestampMillis(1000)
+                .setPropertyString("content", "Some note")
+                .setPropertyString("carrier", "Network Inc")
+                .build();
+        assertThat(documents).containsExactly(expectedText);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
index 6bcc933..718aaa2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
@@ -26,7 +26,6 @@
 
 import java.util.concurrent.ExecutorService;
 
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
 public class AppSearchSessionLocalInternalTest extends AppSearchSessionInternalTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index 4165efe..76f32b4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -26,11 +26,8 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import org.junit.Test;
-
 import java.util.concurrent.ExecutorService;
 
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AppSearchSessionPlatformInternalTest extends AppSearchSessionInternalTestBase {
     @Override
@@ -48,10 +45,4 @@
                 new PlatformStorage.SearchContext.Builder(context, dbName)
                         .setWorkerExecutor(executor).build());
     }
-
-    @Override
-    @Test
-    public void testSearchSuggestion_propertyFilter() throws Exception {
-        // TODO(b/227356108) enable the test when suggestion is ready in platform.
-    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index 7859463..fc5be8f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -18,9 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.os.Bundle;
 import android.os.Parcel;
 
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -44,7 +45,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeBundle(inDoc.getBundle());
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
@@ -52,11 +53,13 @@
         Parcel outParcel = Parcel.obtain();
         outParcel.unmarshall(data, 0, data.length);
         outParcel.setDataPosition(0);
-        Bundle outBundle = outParcel.readBundle();
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel documentParcel =
+                outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
         outParcel.recycle();
 
         // Compare results
-        GenericDocument outDoc = new GenericDocument(outBundle);
+        GenericDocument outDoc = new GenericDocument(documentParcel);
         assertThat(inDoc).isEqualTo(outDoc);
         assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
         assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
@@ -83,7 +86,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeBundle(inDoc.getBundle());
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
@@ -91,11 +94,13 @@
         Parcel outParcel = Parcel.obtain();
         outParcel.unmarshall(data, 0, data.length);
         outParcel.setDataPosition(0);
-        Bundle outBundle = outParcel.readBundle();
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel documentParcel =
+                outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
         outParcel.recycle();
 
         // Compare results
-        GenericDocument outDoc = new GenericDocument(outBundle);
+        GenericDocument outDoc = new GenericDocument(documentParcel);
         assertThat(inDoc).isEqualTo(outDoc);
         assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
         assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
@@ -112,47 +117,23 @@
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
                 .setScore(42)
                 .setPropertyString("propString", "Hello")
-                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
-                .setPropertyDocument(
-                        "propDocument",
-                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
-                                .setPropertyString("propString", "Goodbye")
-                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
-                                .build())
                 .build();
 
         GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class3", "Class4")))
-                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
-                .setPropertyDocument(
-                        "propDocument",
-                        new GenericDocument.Builder<>("namespace", "id3", "schema3")
-                                .setPropertyString("propString", "Bye")
-                                .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
-                                .build())
+                .setPropertyString("propString", "Bye")
                 .build();
 
         // Check that the original GenericDocument is unmodified.
         assertThat(oldDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
         assertThat(oldDoc.getScore()).isEqualTo(42);
         assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
-        assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
-        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
-                .isEqualTo("Goodbye");
-        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
-                .isEqualTo(new byte[][]{{3, 4}});
 
         // Check that the new GenericDocument has modified the original fields correctly.
         assertThat(newDoc.getParentTypes()).isEqualTo(Arrays.asList("Class3", "Class4"));
-        assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
-        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
-                .isEqualTo("Bye");
-        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
-                .isEqualTo(new byte[][]{{5, 6}});
+        assertThat(newDoc.getPropertyString("propString")).isEqualTo("Bye");
 
         // Check that the new GenericDocument copies fields that aren't set.
         assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
-        assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
-                "propString"));
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
new file mode 100644
index 0000000..c63e53e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class InternalVisibilityConfigTest {
+
+    @Test
+    public void testVisibilityConfig_setNotDisplayBySystem() {
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+                .setNotDisplayedBySystem(true).build();
+
+        assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+    }
+
+    @Test
+    public void testVisibilityConfig_setVisibilityConfig() {
+        String visibleToPackage1 = "com.example.package";
+        byte[] visibleToPackageCert1 = new byte[32];
+        String visibleToPackage2 = "com.example.package2";
+        byte[] visibleToPackageCert2 = new byte[32];
+
+        SchemaVisibilityConfig innerConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(
+                        new PackageIdentifier(visibleToPackage1, visibleToPackageCert1))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig innerConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(
+                        new PackageIdentifier(visibleToPackage2, visibleToPackageCert2))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+                .addVisibleToConfig(innerConfig1)
+                .addVisibleToConfig(innerConfig2)
+                .build();
+
+        assertThat(visibilityConfig.getVisibleToConfigs())
+                .containsExactly(innerConfig1, innerConfig2);
+    }
+
+    @Test
+    public void testToInternalVisibilityConfig() {
+        byte[] packageSha256Cert = new byte[32];
+        packageSha256Cert[0] = 24;
+        packageSha256Cert[8] = 23;
+        packageSha256Cert[16] = 22;
+        packageSha256Cert[24] = 21;
+
+        // Create a SetSchemaRequest for testing
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("testSchema").build())
+                .setSchemaTypeDisplayedBySystem("testSchema", false)
+                .setSchemaTypeVisibilityForPackage("testSchema", /*visible=*/true,
+                        new PackageIdentifier("com.example.test", packageSha256Cert))
+                .setPubliclyVisibleSchema("testSchema",
+                        new PackageIdentifier("com.example.test1", packageSha256Cert))
+                .build();
+
+        // Convert the SetSchemaRequest to GenericDocument map
+        List<InternalVisibilityConfig> visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+        // Check if the conversion is correct
+        assertThat(visibilityConfigs).hasSize(1);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+        assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(visibilityConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(new PackageIdentifier("com.example.test", packageSha256Cert));
+        assertThat(visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage())
+                .isNotNull();
+        assertThat(
+                visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                        .getPackageName())
+                .isEqualTo("com.example.test1");
+        assertThat(
+                visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                        .getSha256Certificate())
+                .isEqualTo(packageSha256Cert);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
new file mode 100644
index 0000000..9f79469
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class JoinSpecInternalTest {
+    @Test
+    public void testJoinSpecBuilderCopyConstructor() {
+        JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+                .setMaxJoinedResultCount(10)
+                .setNestedSearch("nestedQuery", new SearchSpec.Builder().build())
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+        JoinSpec joinSpecCopy = new JoinSpec.Builder(joinSpec).build();
+        assertThat(joinSpecCopy.getMaxJoinedResultCount()).isEqualTo(
+                joinSpec.getMaxJoinedResultCount());
+        assertThat(joinSpecCopy.getChildPropertyExpression()).isEqualTo(
+                joinSpec.getChildPropertyExpression());
+        assertThat(joinSpecCopy.getNestedQuery()).isEqualTo(joinSpec.getNestedQuery());
+        assertThat(joinSpecCopy.getNestedSearchSpec()).isNotNull();
+        assertThat(joinSpecCopy.getAggregationScoringStrategy()).isEqualTo(
+                joinSpec.getAggregationScoringStrategy());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
new file mode 100644
index 0000000..bec84cb
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SearchResultInternalTest {
+    @Test
+    public void testSearchResultBuilderCopyConstructor() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .setRankingSignal(1.23)
+                .addJoinedResult(new SearchResult.Builder("pkg1", "db1").setGenericDocument(
+                        document).build())
+                .addJoinedResult(new SearchResult.Builder("pkg2", "db2").setGenericDocument(
+                        document).build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath1").build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath2").build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath3").build())
+                .build();
+        SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+        assertThat(searchResultCopy.getGenericDocument()).isEqualTo(
+                searchResult.getGenericDocument());
+        assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+        // Specifically test JoinedResults and MatchInfos with different sizes since briefly had
+        // a bug where we looped through joinedResults using matchInfos.size()
+        assertThat(searchResultCopy.getJoinedResults().size()).isEqualTo(
+                searchResult.getJoinedResults().size());
+        assertThat(searchResultCopy.getJoinedResults().get(0).getPackageName()).isEqualTo("pkg1");
+        assertThat(searchResultCopy.getJoinedResults().get(0).getDatabaseName()).isEqualTo("db1");
+        assertThat(searchResultCopy.getJoinedResults().get(1).getPackageName()).isEqualTo("pkg2");
+        assertThat(searchResultCopy.getJoinedResults().get(1).getDatabaseName()).isEqualTo("db2");
+        assertThat(searchResultCopy.getMatchInfos().size()).isEqualTo(
+                searchResult.getMatchInfos().size());
+        assertThat(searchResultCopy.getMatchInfos().get(0).getPropertyPath()).isEqualTo(
+                "propertyPath1");
+        assertThat(searchResultCopy.getMatchInfos().get(1).getPropertyPath()).isEqualTo(
+                "propertyPath2");
+        assertThat(searchResultCopy.getMatchInfos().get(2).getPropertyPath()).isEqualTo(
+                "propertyPath3");
+    }
+
+    @Test
+    public void testSearchResultBuilderCopyConstructor_informationalRankingSignal() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .setRankingSignal(1.23)
+                .addInformationalRankingSignal(2)
+                .addInformationalRankingSignal(3)
+                .build();
+        SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+        assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+        assertThat(searchResultCopy.getInformationalRankingSignals()).isEqualTo(
+                searchResult.getInformationalRankingSignals());
+    }
+
+    @Test
+    public void testSearchResultBuilder_clearJoinedResults() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .addJoinedResult(new SearchResult.Builder("pkg", "db").setGenericDocument(
+                        document).build())
+                .clearJoinedResults()
+                .build();
+        assertThat(searchResult.getJoinedResults()).isEmpty();
+    }
+
+    @Test
+    public void testMatchInfoBuilderCopyConstructor() {
+        SearchResult.MatchRange exactMatchRange = new SearchResult.MatchRange(3, 8);
+        SearchResult.MatchRange submatchRange = new SearchResult.MatchRange(3, 5);
+        SearchResult.MatchRange snippetMatchRange = new SearchResult.MatchRange(1, 10);
+        SearchResult.MatchInfo matchInfo =
+                new SearchResult.MatchInfo.Builder("propertyPath1")
+                        .setExactMatchRange(exactMatchRange)
+                        .setSubmatchRange(submatchRange)
+                        .setSnippetRange(snippetMatchRange).build();
+        SearchResult.MatchInfo matchInfoCopy =
+                new SearchResult.MatchInfo.Builder(matchInfo).build();
+        assertThat(matchInfoCopy.getPropertyPath()).isEqualTo("propertyPath1");
+        assertThat(matchInfoCopy.getExactMatchRange()).isEqualTo(exactMatchRange);
+        assertThat(matchInfoCopy.getSubmatchRange()).isEqualTo(submatchRange);
+        assertThat(matchInfoCopy.getSnippetRange()).isEqualTo(snippetMatchRange);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
new file mode 100644
index 0000000..e37d1f854
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchResultPageInternalTest {
+    @Test
+    public void testSearchResultPage() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        List<SearchResult> results = Arrays.asList(
+                new SearchResult.Builder("package1", "database1").setGenericDocument(
+                        document).build(),
+                new SearchResult.Builder("package2", "database2").setGenericDocument(
+                        document).build()
+        );
+        SearchResultPage searchResultPage = new SearchResultPage(/*nextPageToken=*/ 123, results);
+        assertThat(searchResultPage.getNextPageToken()).isEqualTo(123);
+        List<SearchResult> searchResults = searchResultPage.getResults();
+        assertThat(searchResults).hasSize(2);
+        assertThat(searchResults.get(0).getPackageName()).isEqualTo("package1");
+        assertThat(searchResults.get(0).getDatabaseName()).isEqualTo("database1");
+        assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(document);
+        assertThat(searchResults.get(1).getPackageName()).isEqualTo("package2");
+        assertThat(searchResults.get(1).getDatabaseName()).isEqualTo("database2");
+        assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(document);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index ed00e5a..e733fc3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -18,22 +18,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
-import android.os.Bundle;
-
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 
 import org.junit.Test;
 
-import java.util.List;
-import java.util.Map;
+import java.util.Arrays;
 
 /** Tests for private APIs of {@link SearchSpec}. */
 public class SearchSpecInternalTest {
 
     @Test
-    public void testGetBundle() {
+    public void testSearchSpecBuilder() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                 .addFilterNamespaces("namespace1", "namespace2")
@@ -50,28 +45,135 @@
                 .setListFilterQueryLanguageEnabled(true)
                 .build();
 
-        Bundle bundle = searchSpec.getBundle();
-        assertThat(bundle.getInt(SearchSpec.TERM_MATCH_TYPE_FIELD))
-                .isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
-        assertThat(bundle.getStringArrayList(SearchSpec.NAMESPACE_FIELD)).containsExactly(
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+        assertThat(searchSpec.getFilterNamespaces()).containsExactly(
                 "namespace1", "namespace2");
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
+        assertThat(searchSpec.getFilterSchemas()).containsExactly(
                 "schemaTypes1", "schemaTypes2");
-        assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)).containsExactly(
+        assertThat(searchSpec.getFilterPackageNames()).containsExactly(
                 "package1", "package2");
-        assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5);
-        assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10);
-        assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15);
-        assertThat(bundle.getInt(SearchSpec.NUM_PER_PAGE_FIELD)).isEqualTo(42);
-        assertThat(bundle.getInt(SearchSpec.ORDER_FIELD)).isEqualTo(SearchSpec.ORDER_ASCENDING);
-        assertThat(bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD))
+        assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
+        assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
+        assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
+        assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
                 .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
-        assertThat(bundle.getStringArrayList(SearchSpec.ENABLED_FEATURES_FIELD)).containsExactly(
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
                 Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
                 Features.LIST_FILTER_QUERY_LANGUAGE);
     }
 
     @Test
+    public void testSearchSpecBuilderCopyConstructor() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterPackageNames("package1", "package2")
+                .addFilterProperties("schemaTypes1", Arrays.asList("path1", "path2"))
+                .addProjection("schemaTypes1", Arrays.asList("path1", "path2"))
+                .setPropertyWeights("schemaTypes1", ImmutableMap.of("path1", 1.0, "path2", 2.0))
+                .setSnippetCount(5)
+                .setSnippetCountPerProperty(10)
+                .setMaxSnippetSize(15)
+                .setResultCountPerPage(42)
+                .setOrder(SearchSpec.ORDER_ASCENDING)
+                .setRankingStrategy("advancedExpression")
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, 10)
+                .setSearchSourceLogTag("searchSourceLogTag")
+                .build();
+
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getTermMatch()).isEqualTo(searchSpec.getTermMatch());
+        assertThat(searchSpecCopy.getFilterNamespaces()).isEqualTo(
+                searchSpec.getFilterNamespaces());
+        assertThat(searchSpecCopy.getFilterSchemas()).isEqualTo(searchSpecCopy.getFilterSchemas());
+        assertThat(searchSpecCopy.getFilterPackageNames()).isEqualTo(
+                searchSpec.getFilterPackageNames());
+        assertThat(searchSpecCopy.getFilterProperties()).isEqualTo(
+                searchSpec.getFilterProperties());
+        assertThat(searchSpecCopy.getProjections()).isEqualTo(searchSpec.getProjections());
+        assertThat(searchSpecCopy.getPropertyWeights()).isEqualTo(searchSpec.getPropertyWeights());
+        assertThat(searchSpecCopy.getSnippetCount()).isEqualTo(searchSpec.getSnippetCount());
+        assertThat(searchSpecCopy.getSnippetCountPerProperty()).isEqualTo(
+                searchSpec.getSnippetCountPerProperty());
+        assertThat(searchSpecCopy.getMaxSnippetSize()).isEqualTo(searchSpec.getMaxSnippetSize());
+        assertThat(searchSpecCopy.getResultCountPerPage()).isEqualTo(
+                searchSpec.getResultCountPerPage());
+        assertThat(searchSpecCopy.getOrder()).isEqualTo(searchSpec.getOrder());
+        assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+        assertThat(searchSpecCopy.getEnabledFeatures()).containsExactlyElementsIn(
+                searchSpec.getEnabledFeatures());
+        assertThat(searchSpecCopy.getResultGroupingTypeFlags()).isEqualTo(
+                searchSpec.getResultGroupingTypeFlags());
+        assertThat(searchSpecCopy.getResultGroupingLimit()).isEqualTo(
+                searchSpec.getResultGroupingLimit());
+        assertThat(searchSpecCopy.getJoinSpec()).isEqualTo(searchSpec.getJoinSpec());
+        assertThat(searchSpecCopy.getAdvancedRankingExpression()).isEqualTo(
+                searchSpec.getAdvancedRankingExpression());
+        assertThat(searchSpecCopy.getSearchSourceLogTag()).isEqualTo(
+                searchSpec.getSearchSourceLogTag());
+    }
+
+    @Test
+    public void testSearchSpecBuilderCopyConstructor_embeddingSearch() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(embedding1, embedding2)
+                .build();
+
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures()).containsExactlyElementsIn(
+                searchSpec.getEnabledFeatures());
+        assertThat(searchSpecCopy.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                searchSpec.getDefaultEmbeddingSearchMetricType());
+        assertThat(searchSpecCopy.getSearchEmbeddings()).containsExactlyElementsIn(
+                searchSpec.getSearchEmbeddings());
+    }
+
+    @Test
+    public void testSearchSpecBuilderCopyConstructor_informationalRankingExpressions() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy("advancedExpression")
+                .addInformationalRankingExpressions("this.relevanceScore()")
+                .build();
+
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+        assertThat(searchSpecCopy.getAdvancedRankingExpression()).isEqualTo(
+                searchSpec.getAdvancedRankingExpression());
+        assertThat(searchSpecCopy.getInformationalRankingExpressions()).isEqualTo(
+                searchSpec.getInformationalRankingExpressions());
+    }
+
+    // TODO(b/309826655): Flag guard this test.
+    @Test
+    public void testGetBundle_hasProperty() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION);
+    }
+
+    @Test
     public void testBuildMultipleSearchSpecs() {
         SearchSpec.Builder builder = new SearchSpec.Builder();
         SearchSpec searchSpec1 = builder.build();
@@ -90,37 +192,46 @@
                 Features.VERBATIM_SEARCH, Features.LIST_FILTER_QUERY_LANGUAGE);
     }
 
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
     @Test
-    public void testGetPropertyFiltersTypePropertyMasks() {
+    public void testGetEnabledFeatures_embeddingSearch() {
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
-                .addFilterProperties("TypeB", ImmutableList.of("field7"))
-                .addFilterProperties("TypeC", ImmutableList.of())
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .setEmbeddingSearchEnabled(true)
                 .build();
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+                FeatureConstants.EMBEDDING_SEARCH);
 
-        Map<String, List<String>> typePropertyPathMap = searchSpec.getFilterProperties();
-        assertThat(typePropertyPathMap.keySet())
-                .containsExactly("TypeA", "TypeB", "TypeC");
-        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
-        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
-        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+                FeatureConstants.EMBEDDING_SEARCH);
     }
 
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
     @Test
-    public void testBuilder_throwsException_whenTypePropertyFilterNotInSchemaFilter() {
-        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterSchemas("Schema1", "Schema2")
-                .addFilterPropertyPaths("Schema3", ImmutableList.of(
-                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
+    public void testGetEnabledFeatures_tokenize() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE,
+                FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
 
-        IllegalStateException exception =
-                assertThrows(IllegalStateException.class, searchSpecBuilder::build);
-        assertThat(exception.getMessage())
-                .isEqualTo("The schema: Schema3 exists in the property filter but doesn't"
-                        + " exist in the schema filter.");
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE,
+                FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
deleted file mode 100644
index bc68f37..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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 com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Test;
-
-// TODO(b/228240987) delete this test when we support property restrict for multiple terms
-public class SearchSuggestionSpecInternalTest {
-
-    @Test
-    public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
-        SearchSuggestionSpec searchSuggestionSpec =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .setRankingStrategy(SearchSuggestionSpec
-                                .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
-                        .addFilterSchemas("Person", "Email")
-                        .addFilterSchemas(ImmutableList.of("Foo"))
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
-                        .addFilterPropertyPaths("Foo",
-                                ImmutableList.of(new PropertyPath("Bar")))
-                        .build();
-
-        assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
-        assertThat(searchSuggestionSpec.getFilterSchemas())
-                .containsExactly("Person", "Email", "Foo");
-        assertThat(searchSuggestionSpec.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
-                        "Foo",  ImmutableList.of("Bar"));
-    }
-
-    @Test
-    public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
-        IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .addFilterSchemas("Person")
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
-                        .build());
-        assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
-                + "property filter but doesn't exist in the schema filter.");
-    }
-
-    @Test
-    public void testRebuild_withPropertyFilter() throws Exception {
-        SearchSuggestionSpec.Builder builder =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .addFilterSchemas("Person", "Email")
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
-
-        SearchSuggestionSpec original = builder.build();
-
-        builder.addFilterSchemas("Message", "Foo")
-                .addFilterProperties("Foo", ImmutableList.of("Bar"));
-        SearchSuggestionSpec rebuild = builder.build();
-
-        assertThat(original.getMaximumResultCount()).isEqualTo(123);
-        assertThat(original.getFilterSchemas())
-                .containsExactly("Person", "Email");
-        assertThat(original.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"));
-
-        assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
-        assertThat(rebuild.getFilterSchemas())
-                .containsExactly("Person", "Email", "Message", "Foo");
-        assertThat(rebuild.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
-                        "Foo",  ImmutableList.of("Bar"));
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
index 37e1255..145679a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 
@@ -55,7 +53,7 @@
         assertThat(original.getMigratedTypes()).containsExactly("migrated1");
         assertThat(original.getMigrationFailures()).containsExactly(failure1);
 
-        SetSchemaResponse rebuild = original.toBuilder()
+        SetSchemaResponse rebuild = new SetSchemaResponse.Builder(original)
                         .addDeletedType("delete2")
                         .addIncompatibleType("incompatible2")
                         .addMigratedType("migrated2")
@@ -82,7 +80,6 @@
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setDeletionPropagation(true)
                         .build())
                 .build();
 
@@ -95,20 +92,5 @@
                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
         assertThat(((StringPropertyConfig) properties.get(0)).getJoinableValueType())
                 .isEqualTo(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-        assertThat(((StringPropertyConfig) properties.get(0)).getDeletionPropagation())
-                .isEqualTo(true);
-    }
-
-    // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
-    @Test
-    public void testStringPropertyConfig_setJoinableProperty_deletePropagationError() {
-        final StringPropertyConfig.Builder builder =
-                new StringPropertyConfig.Builder("qualifiedId")
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .setDeletionPropagation(true);
-        IllegalStateException e =
-                assertThrows(IllegalStateException.class, () -> builder.build());
-        assertThat(e).hasMessageThat().contains(
-                "Cannot set deletion propagation without setting a joinable value type");
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 4118c45..0e4952c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -25,13 +25,23 @@
 import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.PropertyPath;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.List;
 
 public class AppSearchSchemaCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void testInvalidEnums() {
         StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
@@ -165,6 +175,243 @@
     }
 
     @Test
+    public void testParentTypes() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("EmailMessage")
+                        .addParentType("Email")
+                        .addParentType("Message")
+                        .build();
+        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+    }
+
+    @Test
+    public void testDuplicateParentTypes() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("EmailMessage")
+                        .addParentType("Email")
+                        .addParentType("Message")
+                        .addParentType("Email")
+                        .build();
+        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+    }
+
+    @Test
+    public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {
+        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+                        .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")
+                        .build();
+        assertThat(documentPropertyConfig.getIndexableNestedProperties())
+                .containsExactly("prop1", "prop2", "prop1.prop2");
+    }
+
+    @Test
+    public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {
+        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+                        .addIndexableNestedPropertyPaths(
+                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
+                        .build();
+        assertThat(documentPropertyConfig.getIndexableNestedProperties())
+                .containsExactly("prop1", "prop1.prop2");
+    }
+
+    @Test
+    public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {
+        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
+                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+                        .addIndexableNestedPropertyPaths(
+                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
+                        .addIndexableNestedProperties("prop1")
+                        .build();
+        assertThat(documentPropertyConfig.getIndexableNestedProperties())
+                .containsExactly("prop1", "prop1.prop2");
+    }
+
+    @Test
+    public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {
+        AppSearchSchema.DocumentPropertyConfig.Builder builder =
+                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
+                        .addIndexableNestedProperties("prop1");
+        AppSearchSchema.DocumentPropertyConfig config1 = builder.build();
+        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+
+        builder.addIndexableNestedProperties("prop2");
+        AppSearchSchema.DocumentPropertyConfig config2 = builder.build();
+        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
+        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+
+        builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));
+        AppSearchSchema.DocumentPropertyConfig config3 = builder.build();
+        assertThat(config3.getIndexableNestedProperties())
+                .containsExactly("prop1", "prop2", "prop3");
+        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
+        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
+    }
+
+    @Test
+    public void testPropertyConfig() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("Test")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("string")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("long")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.LongPropertyConfig
+                                                        .INDEXING_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.LongPropertyConfig
+                                                        .INDEXING_TYPE_RANGE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "document1", AppSearchEmail.SCHEMA_TYPE)
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "document2", AppSearchEmail.SCHEMA_TYPE)
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(false)
+                                        .addIndexableNestedProperties("path1", "path2", "path3")
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setJoinableValueType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setJoinableValueType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .build();
+
+        assertThat(schema.getSchemaType()).isEqualTo("Test");
+        List<PropertyConfig> properties = schema.getProperties();
+        assertThat(properties).hasSize(10);
+
+        assertThat(properties.get(0).getName()).isEqualTo("string");
+        assertThat(properties.get(0).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
+                .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(
+                0)).getTokenizerType())
+                .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+        assertThat(properties.get(1).getName()).isEqualTo("long");
+        assertThat(properties.get(1).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
+                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);
+
+        assertThat(properties.get(2).getName()).isEqualTo("indexableLong");
+        assertThat(properties.get(2).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())
+                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
+
+        assertThat(properties.get(3).getName()).isEqualTo("double");
+        assertThat(properties.get(3).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
+
+        assertThat(properties.get(4).getName()).isEqualTo("boolean");
+        assertThat(properties.get(4).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
+
+        assertThat(properties.get(5).getName()).isEqualTo("bytes");
+        assertThat(properties.get(5).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
+
+        assertThat(properties.get(6).getName()).isEqualTo("document1");
+        assertThat(properties.get(6).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(
+                ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))
+                        .shouldIndexNestedProperties())
+                .isEqualTo(true);
+
+        assertThat(properties.get(7).getName()).isEqualTo("document2");
+        assertThat(properties.get(7).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(
+                ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
+                        .shouldIndexNestedProperties())
+                .isEqualTo(false);
+        assertThat(
+                ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
+                        .getIndexableNestedProperties())
+                .containsExactly("path1", "path2", "path3");
+
+        assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");
+        assertThat(properties.get(8).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(
+                ((AppSearchSchema.StringPropertyConfig) properties.get(8))
+                        .getJoinableValueType())
+                .isEqualTo(
+                        AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+
+        assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");
+        assertThat(properties.get(9).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(
+                ((AppSearchSchema.StringPropertyConfig) properties.get(9))
+                        .getJoinableValueType())
+                .isEqualTo(
+                        AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
+    }
+
+    @Test
     public void testInvalidStringPropertyConfigsTokenizerNone() {
         // Everything should work fine with the defaults.
         final StringPropertyConfig.Builder builder =
@@ -195,6 +442,57 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testEquals_failure_differentDescription() {
+        AppSearchSchema.Builder schemaBuilder =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build());
+        AppSearchSchema schema1 = schemaBuilder.build();
+        AppSearchSchema schema2 =
+                schemaBuilder.setDescription("Mail, but like with an 'e'").build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testEquals_failure_differentPropertyDescription() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the contents of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                .build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("The beginning of a message.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                .build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
     public void testInvalidStringPropertyConfigsTokenizerNonNone() {
         // Setting indexing type to be NONE with tokenizer type PLAIN or VERBATIM or RFC822 should
         // fail. Regardless of whether NONE is set explicitly or just kept as default.
@@ -250,162 +548,253 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
     public void testAppSearchSchema_toString() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
-                .addProperty(new StringPropertyConfig.Builder("string1")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string2")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string3")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string4")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string5")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("qualifiedId1")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("qualifiedId2")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .build())
-                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
-                        .build())
-                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
-                        .build())
-                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
-                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .build())
-                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
-                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                        "document", AppSearchEmail.SCHEMA_TYPE)
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .setShouldIndexNestedProperties(true)
-                        .build())
-                .build();
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .setDescription("a test schema")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string1")
+                                        .setDescription("first string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string2")
+                                        .setDescription("second string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string3")
+                                        .setDescription("third string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string4")
+                                        .setDescription("fourth string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string5")
+                                        .setDescription("fifth string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("qualifiedId1")
+                                        .setDescription("first qualifiedId")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setJoinableValueType(
+                                                StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("qualifiedId2")
+                                        .setDescription("second qualifiedId")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setJoinableValueType(
+                                                StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("long")
+                                        .setDescription("a long")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+                                        .setDescription("an indexed long")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                                        .setDescription("a double")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                                        .setDescription("a boolean")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                                        .setDescription("some bytes")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                                "document", AppSearchEmail.SCHEMA_TYPE)
+                                        .setDescription("a document")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
 
         String schemaString = schema.toString();
 
-        String expectedString = "{\n"
-                + "  schemaType: \"testSchema\",\n"
-                + "  properties: [\n"
-                + "    {\n"
-                + "      name: \"boolean\",\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_BOOLEAN,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"bytes\",\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_BYTES,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"document\",\n"
-                + "      shouldIndexNestedProperties: true,\n"
-                + "      schemaType: \"builtin:Email\",\n"
-                + "      cardinality: CARDINALITY_REPEATED,\n"
-                + "      dataType: DATA_TYPE_DOCUMENT,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"double\",\n"
-                + "      cardinality: CARDINALITY_REPEATED,\n"
-                + "      dataType: DATA_TYPE_DOUBLE,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"indexableLong\",\n"
-                + "      indexingType: INDEXING_TYPE_RANGE,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_LONG,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"long\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_LONG,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"qualifiedId1\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"qualifiedId2\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string1\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string2\",\n"
-                + "      indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string3\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string4\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string5\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_RFC822,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    }\n"
-                + "  ]\n"
-                + "}";
+        String expectedString =
+                "{\n"
+                        + "  schemaType: \"testSchema\",\n"
+                        + "  description: \"a test schema\",\n"
+                        + "  properties: [\n"
+                        + "    {\n"
+                        + "      name: \"boolean\",\n"
+                        + "      description: \"a boolean\",\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_BOOLEAN,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"bytes\",\n"
+                        + "      description: \"some bytes\",\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_BYTES,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"document\",\n"
+                        + "      description: \"a document\",\n"
+                        + "      shouldIndexNestedProperties: true,\n"
+                        + "      schemaType: \"builtin:Email\",\n"
+                        + "      cardinality: CARDINALITY_REPEATED,\n"
+                        + "      dataType: DATA_TYPE_DOCUMENT,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"double\",\n"
+                        + "      description: \"a double\",\n"
+                        + "      cardinality: CARDINALITY_REPEATED,\n"
+                        + "      dataType: DATA_TYPE_DOUBLE,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"indexableLong\",\n"
+                        + "      description: \"an indexed long\",\n"
+                        + "      indexingType: INDEXING_TYPE_RANGE,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_LONG,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"long\",\n"
+                        + "      description: \"a long\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_LONG,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"qualifiedId1\",\n"
+                        + "      description: \"first qualifiedId\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"qualifiedId2\",\n"
+                        + "      description: \"second qualifiedId\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string1\",\n"
+                        + "      description: \"first string\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string2\",\n"
+                        + "      description: \"second string\",\n"
+                        + "      indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string3\",\n"
+                        + "      description: \"third string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string4\",\n"
+                        + "      description: \"fourth string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string5\",\n"
+                        + "      description: \"fifth string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_RFC822,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    }\n"
+                        + "  ]\n"
+                        + "}";
+
+        String[] lines = expectedString.split("\n");
+        for (String line : lines) {
+            assertThat(schemaString).contains(line);
+        }
+    }
+
+    @Test
+    public void testAppSearchSchema_toStringNoDescriptionSet() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string1")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                                        .build())
+                        .build();
+
+        String schemaString = schema.toString();
+
+        String expectedString =
+                          "{\n"
+                        + "  schemaType: \"testSchema\",\n"
+                        + "  description: \"\",\n"
+                        + "  properties: [\n"
+                        + "    {\n"
+                        + "      name: \"string1\",\n"
+                        + "      description: \"\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    }\n"
+                        + "  ]\n"
+                        + "}";
 
         String[] lines = expectedString.split("\n");
         for (String line : lines) {
@@ -466,4 +855,120 @@
                         "DocumentIndexingConfig#shouldIndexNestedProperties is required to be false"
                                 + " when one or more indexableNestedProperties are provided.");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("Test")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("string")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.LongPropertyConfig
+                                                        .INDEXING_TYPE_RANGE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "document1", AppSearchEmail.SCHEMA_TYPE)
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.EmbeddingPropertyConfig
+                                                        .INDEXING_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.EmbeddingPropertyConfig.Builder(
+                                        "indexableEmbedding")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.EmbeddingPropertyConfig
+                                                        .INDEXING_TYPE_SIMILARITY)
+                                        .build())
+                        .build();
+
+        assertThat(schema.getSchemaType()).isEqualTo("Test");
+        List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
+        assertThat(properties).hasSize(5);
+
+        assertThat(properties.get(0).getName()).isEqualTo("string");
+        assertThat(properties.get(0).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
+        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
+                .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
+        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
+                .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
+
+        assertThat(properties.get(1).getName()).isEqualTo("indexableLong");
+        assertThat(properties.get(1).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
+                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
+
+        assertThat(properties.get(2).getName()).isEqualTo("document1");
+        assertThat(properties.get(2).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(2)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(
+                ((AppSearchSchema.DocumentPropertyConfig) properties.get(2))
+                        .shouldIndexNestedProperties())
+                .isEqualTo(true);
+
+        assertThat(properties.get(3).getName()).isEqualTo("embedding");
+        assertThat(properties.get(3).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(3)).getIndexingType())
+                .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+
+        assertThat(properties.get(4).getName()).isEqualTo("indexableEmbedding");
+        assertThat(properties.get(4).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(4)).getIndexingType())
+                .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig_defaultValues() {
+        AppSearchSchema.EmbeddingPropertyConfig builder =
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("test").build();
+        assertThat(builder.getIndexingType()).isEqualTo(
+                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.getCardinality()).isEqualTo(
+                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig_setIndexingType() {
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(5).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(2).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(-1).build());
+    }
 }
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 59c17f5..895a58a 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
@@ -154,9 +154,11 @@
     }
 
     @Test
-    public void testSchemaMigration_A_B_C_D() throws Exception {
+    public void test_ForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema and update the version
-        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -166,7 +168,7 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -174,9 +176,11 @@
     }
 
     @Test
-    public void testSchemaMigration_A_B_NC_D() throws Exception {
+    public void testForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema but don't update the version
-        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -186,20 +190,22 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_A_NB_C_D() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -207,13 +213,15 @@
     }
 
     @Test
-    public void testSchemaMigration_A_NB_C_ND() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -221,35 +229,42 @@
     }
 
     @Test
-    public void testSchemaMigration_A_NB_NC_D() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoMigrateIncompatibleTypeSchema =
+                new AppSearchSchema.Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(
+                                backwardsIncompatibleNoMigrateIncompatibleTypeSchema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_B_C_D() throws Exception {
+    public void testNoForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema and update the version
-        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -259,16 +274,18 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_B_NC_D() throws Exception {
+    public void testNoForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema but don't update the version
-        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -278,34 +295,38 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_C_D() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
+                        new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .setVersion(2)     // upgrade version
                                 .build()).get());
@@ -313,14 +334,17 @@
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema =
+                new AppSearchSchema.Builder("testSchema")
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                        new SetSchemaRequest.Builder().addSchemas(
+                                backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .build()).get());
         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
@@ -704,7 +728,7 @@
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
 
-        Migrator migrator_sourceToNowhere = new Migrator() {
+        Migrator migratorSourceToNowhere = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -732,7 +756,7 @@
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setMigrator("sourceSchema", migratorSourceToNowhere)
                         .setVersion(2).build())   // upgrade version
                         .get());
         assertThat(exception).hasMessageThat().contains(
@@ -744,7 +768,7 @@
         exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setMigrator("sourceSchema", migratorSourceToNowhere)
                         .setForceOverride(true)
                         .setVersion(2).build())   // upgrade version
                         .get());
@@ -761,7 +785,7 @@
         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(destinationSchema).setForceOverride(true).build()).get();
 
-        Migrator migrator_nowhereToDestination = new Migrator() {
+        Migrator migratorNowhereToDestination = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -789,7 +813,7 @@
         SetSchemaResponse setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setMigrator("nonExistSchema", migratorNowhereToDestination)
                         .setVersion(2) //  upgrade version
                         .build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -798,7 +822,7 @@
         setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setMigrator("nonExistSchema", migratorNowhereToDestination)
                         .setVersion(2) //  upgrade version
                         .setForceOverride(true).build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -809,7 +833,7 @@
         // set empty schema
         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .setForceOverride(true).build()).get();
-        Migrator migrator_nowhereToNowhere = new Migrator() {
+        Migrator migratorNowhereToNowhere = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -837,7 +861,7 @@
         SetSchemaResponse setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setMigrator("nonExistSchema", migratorNowhereToNowhere)
                         .setVersion(2)  //  upgrade version
                         .build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -846,7 +870,7 @@
         setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setMigrator("nonExistSchema", migratorNowhereToNowhere)
                         .setVersion(2) //  upgrade version
                         .setForceOverride(true).build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -1208,7 +1232,7 @@
         @Override
         public GenericDocument onUpgrade(int currentVersion, int finalVersion,
                 @NonNull GenericDocument document) {
-            GenericDocument.Builder docBuilder =
+            GenericDocument.Builder<?> docBuilder =
                     new GenericDocument.Builder<>("namespace", "id", "TypeB")
                             .setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
             if (currentVersion == 2) {
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 672c0f3..97698a7 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
@@ -41,6 +41,7 @@
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
@@ -51,6 +52,7 @@
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
@@ -60,7 +62,13 @@
 import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.cts.app.customer.EmailDocument;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
 import androidx.appsearch.util.DocumentIdUtil;
 import androidx.collection.ArrayMap;
 import androidx.test.core.app.ApplicationProvider;
@@ -73,6 +81,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -89,6 +98,14 @@
     static final String DB_NAME_1 = "";
     static final String DB_NAME_2 = "testDb2";
 
+    // Since we cannot call non-public API in the cts test, make a copy of these 2 action types, so
+    // we can create taken actions in GenericDocument form.
+    private static final int ACTION_TYPE_SEARCH = 1;
+    private static final int ACTION_TYPE_CLICK = 2;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
 
     private AppSearchSession mDb1;
@@ -170,6 +187,178 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_schemaDescription_notSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SCHEMA_SET_DESCRIPTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+                .setDescription("Unsupported description")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_propertyDescription_notSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SCHEMA_SET_DESCRIPTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setDescription("Unsupported description")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_updateSchemaDescription() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+                .get();
+
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema1);
+
+        // Change the type description.
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("Like mail but with an 'a'.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+                .get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_updatePropertyDescription() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+                .get();
+
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema1);
+
+        // Change the type description.
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("The most important part of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All the other stuff.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+                .get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+    }
+
+    @Test
     public void testSetSchema_updateVersion() throws Exception {
         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
                 .addProperty(new StringPropertyConfig.Builder("subject")
@@ -420,6 +609,46 @@
     }
 
     @Test
+    public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+        // Create schema with invalid cycle: Person -> Organization -> Person... where all
+        // DocumentPropertyConfigs have setShouldIndexNestedProperties(true).
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+        AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class,
+                        () -> mDb1.setSchemaAsync(setSchemaRequest).get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
+    }
+
+    @Test
     public void testSetSchemaWithValidCycle_circularReferencesNotSupported() {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
 
@@ -467,8 +696,6 @@
     }
 // @exportToFramework:endStrip()
 
-// @exportToFramework:startStrip()
-
     /** Test indexing maximum properties into a schema. */
     @Test
     public void testSetSchema_maxProperties() throws Exception {
@@ -485,11 +712,89 @@
         Set<AppSearchSchema> actual1 = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actual1).containsExactly(maxSchema);
 
-        // TODO(b/300135897): Expand test to assert adding more than allowed properties is
-        //  fixed once fixed.
+        schemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        ExecutionException exception = assertThrows(ExecutionException.class, () ->
+                mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                        .addSchemas(schemaBuilder.build()).setForceOverride(true).build()).get());
+        Throwable cause = exception.getCause();
+        assertThat(cause).isInstanceOf(AppSearchException.class);
+        assertThat(cause.getMessage()).isEqualTo("Too many properties to be indexed, max "
+                + "number of properties allowed: " + maxProperties);
     }
 
     @Test
+    public void testSetSchema_maxProperties_nestedSchemas() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+        int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
+        AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+        for (int i = 0; i < maxProperties / 3 + 1; i++) {
+            personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("worksFor",
+                "Organization")
+                .setShouldIndexNestedProperties(false)
+                .build());
+        personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                .setShouldIndexNestedProperties(true)
+                .build());
+
+        AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization");
+        for (int i = 0; i < maxProperties / 3; i++) {
+            orgSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        orgSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                .setShouldIndexNestedProperties(true)
+                .build());
+
+        AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address");
+        for (int i = 0; i < maxProperties / 3; i++) {
+            addressSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+
+        AppSearchSchema personSchema = personSchemaBuilder.build();
+        AppSearchSchema orgSchema = orgSchemaBuilder.build();
+        AppSearchSchema addressSchema = addressSchemaBuilder.build();
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(personSchema, orgSchema, addressSchema)
+                        .build()).get();
+        Set<AppSearchSchema> schemas = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema);
+
+        // Add one more property to bring the number of sections over the max limit
+        personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchemaBuilder.build(), orgSchema, addressSchema)
+                                .setForceOverride(true)
+                                .build()
+                ).get());
+        Throwable cause = exception.getCause();
+        assertThat(cause).isInstanceOf(AppSearchException.class);
+        assertThat(cause.getMessage()).contains("Too many properties to be indexed");
+    }
+
+// @exportToFramework:startStrip()
+
+    @Test
     public void testGetSchema() throws Exception {
         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
                 .addProperty(new StringPropertyConfig.Builder("subject")
@@ -672,6 +977,58 @@
     }
 
     @Test
+    public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+
+        AppSearchSchema noteSchema = new AppSearchSchema.Builder("Note")
+                .addProperty(new StringPropertyConfig.Builder("subject").build()).build();
+        SetSchemaRequest.Builder requestBuilder = new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA, noteSchema)
+                .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false)
+                .setSchemaTypeVisibilityForPackage(
+                        noteSchema.getSchemaType(),
+                        true,
+                        new PackageIdentifier("com.some.package1", new byte[32]))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        noteSchema.getSchemaType(),
+                        Collections.singleton(SetSchemaRequest.READ_SMS));
+        if (mDb1.getFeatures().isFeatureSupported(
+                Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+            requestBuilder.setPubliclyVisibleSchema(
+                    noteSchema.getSchemaType(),
+                    new PackageIdentifier("com.some.package2", new byte[32]));
+        }
+        SetSchemaRequest request = requestBuilder.build();
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        Set<AppSearchSchema> actual = getSchemaResponse.getSchemas();
+        assertThat(actual).hasSize(2);
+        assertThat(actual).isEqualTo(request.getSchemas());
+
+        // Check visibility settings. Schemas without settings shouldn't appear in the result at
+        // all, even with empty maps as values.
+        assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly(noteSchema.getSchemaType());
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
+                .containsExactly(
+                        noteSchema.getSchemaType(),
+                        ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32])));
+        assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        noteSchema.getSchemaType(),
+                        ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
+        if (mDb1.getFeatures().isFeatureSupported(
+                Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+            assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+                    .containsExactly(
+                            noteSchema.getSchemaType(),
+                            new PackageIdentifier("com.some.package2", new byte[32]));
+        }
+    }
+
+    @Test
     public void testGetSchema_visibilitySetting_notSupported() throws Exception {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(
                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
@@ -740,6 +1097,101 @@
     }
 
     @Test
+    public void testSetSchema_publiclyVisible() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+        PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]);
+        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+                .setPubliclyVisibleSchema("builtin:Email", pkg).build();
+
+        mDb1.setSchemaAsync(request).get();
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+        assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+                .isEqualTo(ImmutableMap.of("builtin:Email", pkg));
+
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setSubject("testPut example").build();
+
+        // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we
+        // can make sure they their own documents under the Public ACL.
+        AppSearchBatchResult<String, Void> putResult =
+                checkIsBatchResultSuccess(mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(putResult.getSuccesses()).containsExactly("id1", null);
+        assertThat(putResult.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest getByDocumentIdRequest =
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("id1")
+                        .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, getByDocumentIdRequest);
+        assertThat(outDocuments).hasSize(1);
+        assertThat(outDocuments).containsExactly(email);
+    }
+
+    @Test
+    public void testSetSchema_publiclyVisible_unsupported() {
+        assumeFalse(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("Email").build())
+                .setPubliclyVisibleSchema("Email",
+                        new PackageIdentifier(mContext.getPackageName(), new byte[32])).build();
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(e.getMessage()).isEqualTo("Publicly visible schema are not supported on this "
+                + "AppSearch implementation.");
+    }
+
+    @Test
+    public void testSetSchema_visibleToConfig() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1);
+        PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2);
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .setPubliclyVisibleTargetPackage(pkg1)
+                .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .setPubliclyVisibleTargetPackage(pkg2)
+                .addRequiredPermissions(ImmutableSet.of(3, 4)).build();
+        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+                .addSchemaTypeVisibleToConfig("builtin:Email", config1)
+                .addSchemaTypeVisibleToConfig("builtin:Email", config2)
+                .build();
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs())
+                .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2)));
+    }
+
+    @Test
+    public void testSetSchema_visibleToConfig_unsupported() {
+        assumeFalse(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+
+        SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+                .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("Email").build())
+                .addSchemaTypeVisibleToConfig("Email", config).build();
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(e.getMessage()).isEqualTo("Schema visible to config are not supported on"
+                + " this AppSearch implementation.");
+    }
+
+    @Test
     public void testGetSchema_longPropertyIndexingType() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
@@ -1112,9 +1564,106 @@
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
     }
+
+    @Test
+    public void testPutDocuments_takenActions() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addDocumentClasses(SearchAction.class, ClickAction.class)
+                                .build())
+                .get();
+
+        // Put a SearchAction and ClickAction document
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("query")
+                        .setFetchedResultCount(10)
+                        .build();
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */2000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addTakenActions(searchAction, clickAction)
+                        .build()));
+        assertThat(result.getSuccesses()).containsEntry("search", null);
+        assertThat(result.getSuccesses()).containsEntry("click", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
 // @exportToFramework:endStrip()
 
     @Test
+    public void testPutDocuments_takenActionGenericDocuments() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .build()
+                ).build();
+
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(searchActionSchema, clickActionSchema)
+                        .build()).get();
+
+        // Put search action and click action generic documents.
+        GenericDocument searchAction =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .build();
+        GenericDocument clickAction =
+                new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId")
+                        .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addTakenActionGenericDocuments(searchAction, clickAction)
+                        .build()));
+        assertThat(result.getSuccesses()).containsEntry("search", null);
+        assertThat(result.getSuccesses()).containsEntry("click", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @Test
     public void testUpdateSchema() throws Exception {
         // Schema registration
         AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
@@ -2112,6 +2661,247 @@
         assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
     }
 
+// @exportToFramework:startStrip()
+    @Test
+    public void testQueryRankByClickActions_useTakenAction() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addDocumentClasses(SearchAction.class, ClickAction.class)
+                                .build())
+                .get();
+
+        // Index several email documents
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+
+        String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail1);
+        String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setFetchedResultCount(20)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId1)
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(1)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId2)
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(128)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId1)
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(256)
+                        .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(inEmail1, inEmail2)
+                        .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3)
+                        .build()));
+
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setOrder(SearchSpec.ORDER_DESCENDING)
+                        .addFilterDocumentClasses(ClickAction.class)
+                        .build();
+
+        // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+        // documents returned. It does not affect the number of child documents that are scored.
+        JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+                .setNestedSearch("query:body", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .setMaxJoinedResultCount(0)
+                .build();
+
+        // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+        // query = "body".
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        assertThat(sr).hasSize(2);
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testQueryRankByTakenActions_useTakenActionGenericDocument() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema)
+                        .build())
+                .get();
+
+        // Index several email documents
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+
+        String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail1);
+        String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+        GenericDocument searchAction =
+                new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .build();
+        GenericDocument clickAction1 =
+                new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId1)
+                        .build();
+        GenericDocument clickAction2 =
+                new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId2)
+                        .build();
+        GenericDocument clickAction3 =
+                new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(4000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId1)
+                        .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(inEmail1, inEmail2)
+                        .addTakenActionGenericDocuments(
+                                searchAction, clickAction1, clickAction2, clickAction3)
+                        .build()));
+
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setOrder(SearchSpec.ORDER_DESCENDING)
+                        .addFilterSchemas("builtin:ClickAction")
+                        .build();
+
+        // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+        // documents returned. It does not affect the number of child documents that are scored.
+        JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+                .setNestedSearch("query:body", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .setMaxJoinedResultCount(0)
+                .build();
+
+        // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+        // query = "body".
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        assertThat(sr).hasSize(2);
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+    }
+
     @Test
     public void testQuery_invalidAdvancedRanking() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(
@@ -2267,7 +3057,7 @@
         assertThat(documents).hasSize(1);
         assertThat(documents).containsExactly(inDoc);
 
-        // Query only for non-exist type
+        // Query only for non-existent type
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .addFilterSchemas("nonExistType")
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
@@ -2353,7 +3143,7 @@
         assertThat(documents).hasSize(1);
         assertThat(documents).containsExactly(expectedEmail);
 
-        // Query only for non-exist namespace
+        // Query only for non-existent namespace
         searchResults = mDb1.search("body",
                 new SearchSpec.Builder()
                         .addFilterNamespaces("nonExistNamespace")
@@ -2699,7 +3489,7 @@
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection(
-                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2760,7 +3550,7 @@
         // Query with type property paths {"*", []}
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList())
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2822,7 +3612,7 @@
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection("NonExistentType", Collections.emptyList())
                 .addProjection(
-                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2842,6 +3632,23 @@
     }
 
     @Test
+    public void testSearchSpec_setSourceTag_notSupported() {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG));
+        // UnsupportedOperationException will be thrown with these queries so no need to
+        // define a schema and index document.
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("\"Hello, world!\"", searchSpec));
+        assertThat(exception).hasMessageThat().contains(
+                Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+                        + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testQuery_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -2889,6 +3696,332 @@
     }
 
     @Test
+    public void testQuery_typePropertyFilters() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query with type property filters {"Email", ["subject", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // Only email2 should be returned because email1 doesn't have the term "body" in subject
+        // or to fields
+        assertThat(documents).containsExactly(email2);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
+        // schema has body in its property filter but Email schema doesn't.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // Only the note document should be returned because the email property filter doesn't
+        // allow searching in the body.
+        assertThat(documents).containsExactly(note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will apply to both the Email and Note schema. The email
+        // document should be returned since it has the term "body" in its subject property. The
+        // note document should not be returned since it doesn't have the term "body" in the title
+        // property (subject property is not applicable for Note schema)
+        assertThat(documents).containsExactly(email);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will only apply to the Email schema since Note schema has
+        // its own explicit property filter specified. The email document should be returned since
+        // it has the term "body" in its subject property. The note document should also be returned
+        // since it has the term "body" in the body property.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"NonExistentType": ["to", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The supplied property filters don't apply to either schema types. Both the documents
+        // should be returned since the term "body" is present in at least one of their properties.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersEmpty() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"email": []}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        // The email document should not be returned since the property filter doesn't allow
+        // searching any property.
+        assertThat(documents).containsExactly(note);
+    }
+
+    @Test
     public void testSnippet() throws Exception {
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
@@ -2929,10 +4062,10 @@
         assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
                 + "Another nonsense word that’s used a lot is bar");
         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/32));
+                new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
         assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
         assertThat(matchInfo.getSnippetRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
+                new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
         assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
 
         if (!mDb1.getFeatures().isFeatureSupported(
@@ -2941,7 +4074,7 @@
             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
         } else {
             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/31));
+                    new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
             assertThat(matchInfo.getSubmatch()).isEqualTo("fo");
         }
     }
@@ -3068,7 +4201,7 @@
         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
         assertThat(matchInfo.getFullText()).isEqualTo(japanese);
         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
+                new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
         assertThat(matchInfo.getExactMatch()).isEqualTo("は");
 
         if (!mDb1.getFeatures().isFeatureSupported(
@@ -3077,7 +4210,7 @@
             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
         } else {
             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
+                    new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
             assertThat(matchInfo.getSubmatch()).isEqualTo("は");
         }
     }
@@ -3992,7 +5125,7 @@
         // returned.
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4);
@@ -4001,7 +5134,7 @@
         // be returned ('email4' and 'email2').
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4, inEmail2);
@@ -4011,13 +5144,442 @@
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4, inEmail2);
     }
 
     @Test
+    public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+        // Schema registration
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                .addProperty(
+                    new StringPropertyConfig.Builder("foo")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                            StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                    .addSchemas(AppSearchEmail.SCHEMA)
+                    .addSchemas(genericSchema)
+                    .build())
+                .get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+        AppSearchEmail inEmail5 =
+                new AppSearchEmail.Builder("namespace2", "id5")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
+        GenericDocument inDoc1 =
+                new GenericDocument.Builder<>("namespace3", "id6", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
+        GenericDocument inDoc2 =
+                new GenericDocument.Builder<>("namespace3", "id7", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
+        GenericDocument inDoc3 =
+                new GenericDocument.Builder<>("namespace4", "id8", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
+
+        // Query with per package result grouping. Only the last document 'doc3' should be
+        // returned.
+        SearchResults searchResults =
+                mDb1.search(
+                "body",
+                    new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
+                    .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace result grouping. Two of the last documents in each namespace
+        // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per schema result grouping. Only the last document of each schema type should
+        // be returned ('doc3', 'email5')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+        // Query with per schema result grouping. Only the last two documents of each schema type
+        // should be returned ('doc3', 'doc2', 'email5', 'email4')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per package and per namespace result grouping. Only the last two documents
+        // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+        // 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per package and per schema type result grouping. Only the last document in
+        // each schema type should be returned. ('doc3', 'email5')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+        // Query with per package and per schema type result grouping. Only the last two document in
+        // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+        // Query with per namespace and per schema type result grouping. Only the last document in
+        // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace and per schema type result grouping. Only the last two documents
+        // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+        // 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per namespace, per package and per schema type result grouping. Only the last
+        // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace, per package and per schema type result grouping. Only the last
+        // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
+        // 'email4', 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
+        assumeFalse(
+                mDb1.getFeatures()
+                .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec1 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+                .build();
+        UnsupportedOperationException exception =
+                assertThrows(
+                UnsupportedOperationException.class,
+                    () -> mDb1.search("body", searchSpec1));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec2 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_PACKAGE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec2));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec3 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec3));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec4 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec4));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+    }
+
+    @Test
     public void testIndexNestedDocuments() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -4308,8 +5870,6 @@
                 .build();
         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
 
-        // ListFilterQueryLanguage is enabled so that EXPERIMENTAL_ICING_ADVANCED_QUERY gets enabled
-        // in IcingLib.
         // Disable VERBATIM_SEARCH in the SearchSpec.
         SearchResults searchResults = mDb1.search("\"Hello, world!\"",
                 new SearchSpec.Builder()
@@ -4507,6 +6067,138 @@
     }
 
     @Test
+    public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception {
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+
+        // UnsupportedOperationException will be thrown with these queries so no need to
+        // define a schema and index document.
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("\"Hello, world!\"", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testQuery_hasPropertyFunction() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+                .addProperty(new StringPropertyConfig.Builder("prop1")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("prop2")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).addSchemas(schema).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>(
+                "namespace", "id1", "Schema")
+                .setPropertyString("prop1", "Hello, world!")
+                .build();
+        GenericDocument doc2 = new GenericDocument.Builder<>(
+                "namespace", "id2", "Schema")
+                .setPropertyString("prop2", "Hello, world!")
+                .build();
+        GenericDocument doc3 = new GenericDocument.Builder<>(
+                "namespace", "id3", "Schema")
+                .setPropertyString("prop1", "Hello, world!")
+                .setPropertyString("prop2", "Hello, world!")
+                .build();
+        mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(doc1, doc2, doc3).build()).get();
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        SearchResults searchResults = mDb1.search("hasProperty(\"prop1\")",
+                searchSpec);
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec);
+        page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+
+        searchResults = mDb1.search(
+                "hasProperty(\"prop1\") AND hasProperty(\"prop2\")",
+                searchSpec);
+        page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+    }
+
+    @Test
+    public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+                .addProperty(new StringPropertyConfig.Builder("prop")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).addSchemas(schema).build()).get();
+
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace1", "id1", "Schema")
+                .setPropertyString("prop", "Hello, world!")
+                .build();
+        mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
+
+        // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the
+        // SearchSpec.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(false)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search("hasProperty(\"prop\")",
+                searchSpec);
+        ExecutionException executionException = assertThrows(ExecutionException.class,
+                () -> searchResults.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
+
+        // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec.
+        searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(false)
+                .build();
+        SearchResults searchResults2 = mDb1.search("hasProperty(\"prop\")",
+                searchSpec);
+        executionException = assertThrows(ExecutionException.class,
+                () -> searchResults2.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION");
+    }
+
+    @Test
     public void testQuery_propertyWeightsNotSupported() throws Exception {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
 
@@ -4793,6 +6485,257 @@
     }
 
     @Test
+    public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec only allows searching the viewType property for viewAction
+        // schema type. It also specifies a property filter for Email schema.
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                        .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
+                                ImmutableList.of("subject"))
+                        .build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned, email2 comes first because it has higher number of
+        // joined documents. The property filters for Email schema specified in the nested search
+        // specs don't apply to the outer query (otherwise none of the email documents would have
+        // been returned).
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the join spec, so it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
+        // in the join spec, so it should not be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+        assertThat(sr.get(1).getJoinedResults()).isEmpty();
+    }
+
+    @Test
+    public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec doesn't specify any property filters.
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        // Outer search spec adds property filters for both Email and ViewAction schema
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
+                .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                .build());
+
+        List<SearchResult> sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned as they both satisfy the property filters for Email, email2
+        // comes first because it has higher id lexicographically.
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the outer spec (although those property filters are irrelevant for joined documents),
+        // it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
+        // in the outer spec, but property filters in the outer spec should not apply on joined
+        // documents, so viewAction1 should be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersNotSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
+        // exception is thrown
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        UnsupportedOperationException exception =
+                assertThrows(UnsupportedOperationException.class,
+                        () -> mDb1.search("body", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testSimpleJoin() throws Exception {
         assumeTrue(mDb1.getFeatures()
                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
@@ -4915,7 +6858,7 @@
         assertThrows(UnsupportedOperationException.class, () ->
                 mDb1.searchSuggestionAsync(
                         /*suggestionQueryExpression=*/"t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get());
+                        new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get());
     }
 
     @Test
@@ -4960,14 +6903,14 @@
 
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo, resultThree, resultFour)
                 .inOrder();
 
         // Query first 2 suggestions, and they will be ranked.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
     }
 
@@ -5008,21 +6951,21 @@
         // namespace1 has 2 results.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1").build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
 
         // namespace2 has 1 result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace2").build()).get();
         assertThat(suggestions).containsExactly(resultFoo).inOrder();
 
         // namespace2 and 3 has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace2", "namespace3")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5030,7 +6973,7 @@
         // non exist namespace has empty result
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("nonExistNamespace").build()).get();
         assertThat(suggestions).isEmpty();
     }
@@ -5077,7 +7020,7 @@
         // Only search for namespace1/doc1
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1")
                         .addFilterDocumentIds("namespace1", "id1")
                         .build()).get();
@@ -5086,7 +7029,7 @@
         // Only search for namespace1/doc1 and namespace1/doc2
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1")
                         .addFilterDocumentIds("namespace1", ImmutableList.of("id1", "id2"))
                         .build()).get();
@@ -5095,7 +7038,7 @@
         // Only search for namespace1/doc1 and namespace2/doc3
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1", "namespace2")
                         .addFilterDocumentIds("namespace1", "id1")
                         .addFilterDocumentIds("namespace2", ImmutableList.of("id3"))
@@ -5105,7 +7048,7 @@
         // Only search for namespace1/doc1 and everything in namespace2
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterDocumentIds("namespace1", "id1")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
@@ -5163,21 +7106,21 @@
         // Type1 has 2 results.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type1").build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
 
         // Type2 has 1 result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type2").build()).get();
         assertThat(suggestions).containsExactly(resultFoo).inOrder();
 
         // Type2 and 3 has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type2", "Type3")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5185,7 +7128,7 @@
         // non exist type has empty result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("nonExistType").build()).get();
         assertThat(suggestions).isEmpty();
     }
@@ -5233,13 +7176,13 @@
         // prefix f has 2 results.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
 
         // prefix b has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"b",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultBar, resultBaz);
     }
 
@@ -5285,7 +7228,7 @@
         // rank by NONE, the order should be arbitrary but all terms appear.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_NONE)
                         .build()).get();
@@ -5294,7 +7237,7 @@
         // rank by document count, the order should be term1:3 > term2:2 > term3:1
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
                         .build()).get();
@@ -5303,7 +7246,7 @@
         // rank by term frequency, the order should be term3:5 > term2:4 > term1:3
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
                         .build()).get();
@@ -5348,7 +7291,7 @@
         // prefix t has 3 results.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
 
         // Delete the document
@@ -5359,7 +7302,7 @@
         // now prefix t has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultThree, resultTart);
     }
 
@@ -5397,7 +7340,7 @@
         // prefix t has 3 results.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
 
         // replace the document
@@ -5411,7 +7354,7 @@
         // prefix t has 2 results for now.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultThree, resultTwist);
     }
 
@@ -5448,13 +7391,13 @@
         // database 1 could get suggestion results
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
 
         // database 2 couldn't get suggestion results
         suggestions = mDb2.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).isEmpty();
     }
 
@@ -5488,7 +7431,7 @@
         // Search "bar AND f" only document 1 should match the search.
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
         assertThat(suggestions).containsExactly(barFo);
@@ -5497,7 +7440,7 @@
         // match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar OR cat f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barCatFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
         SearchSuggestionResult barCatFoo =
@@ -5507,7 +7450,7 @@
         // Search for "(bar AND cat) OR f", all documents could match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"(bar cat) OR f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barCatOrFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
         SearchSuggestionResult barCatOrFoo =
@@ -5520,7 +7463,7 @@
         // Search for "-bar f", document2 "cat foo" could and document3 "fool" could match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"-bar f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult noBarFoo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
         SearchSuggestionResult noBarFool =
@@ -5529,6 +7472,162 @@
     }
 
     @Test
+    public void testSearchSuggestion_propertyFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        AppSearchSchema schemaType1 =
+                new AppSearchSchema.Builder("Type1")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertyone")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertytwo")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema schemaType2 =
+                new AppSearchSchema.Builder("Type2")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertythree")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertyfour")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
+                .get();
+
+        // Index documents
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Type1")
+                        .setPropertyString("propertyone", "termone")
+                        .setPropertyString("propertytwo", "termtwo")
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "Type2")
+                        .setPropertyString("propertythree", "termthree")
+                        .setPropertyString("propertyfour", "termfour")
+                        .build();
+
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
+
+        SearchSuggestionResult resultOne =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
+        SearchSuggestionResult resultTwo =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
+        SearchSuggestionResult resultThree =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
+        SearchSuggestionResult resultFour =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
+
+        // Only search for type1/propertyone
+        List<SearchSuggestionResult> suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne);
+
+        // Only search for type1/propertyone and type1/propertytwo
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1")
+                                        .addFilterProperties(
+                                                "Type1",
+                                                ImmutableList.of("propertyone", "propertytwo"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultTwo);
+
+        // Only search for type1/propertyone and type2/propertythree
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1", "Type2")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .addFilterProperties(
+                                                "Type2", ImmutableList.of("propertythree"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultThree);
+
+        // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1", "Type2")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .addFilterPropertyPaths(
+                                                "Type2",
+                                                ImmutableList.of(new PropertyPath("propertyfour")))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultFour);
+
+        // Only search for type1/propertyone and everything in type2
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
+    }
+
+    @Test
+    public void testSearchSuggestion_propertyFilter_notSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+
+        SearchSuggestionSpec searchSuggestionSpec =
+                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                    .addFilterSchemas("Type1")
+                    .addFilterProperties("Type1", ImmutableList.of("property"))
+                    .build();
+
+        // Search suggest with type property filters {"Email", ["property"]} and verify that
+        // unsupported exception is thrown
+        UnsupportedOperationException exception =
+                assertThrows(UnsupportedOperationException.class,
+                        () -> mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t", searchSuggestionSpec).get());
+        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testSearchSuggestion_PropertyRestriction() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
         // Schema registration
@@ -5566,7 +7665,7 @@
         // Search for "bar AND subject:f"
         List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar subject:f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barSubjectFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
         SearchSuggestionResult barSubjectFoo =
@@ -5609,6 +7708,13 @@
         Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actual).hasSize(3);
         assertThat(actual).isEqualTo(request.getSchemas());
+
+        // Check that calling getParentType() for the EmailMessage schema returns Email and Message
+        for (AppSearchSchema schema : actual) {
+            if (schema.getSchemaType().equals("EmailMessage")) {
+                assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+            }
+        }
     }
 
     @Test
@@ -5641,6 +7747,70 @@
     }
 
     @Test
+    public void testGetSchema_indexableNestedPropsList() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
+
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "worksFor", "Organization")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setShouldIndexNestedProperties(false)
+                                        .addIndexableNestedProperties(Collections.singleton("name"))
+                                        .build())
+                        .build();
+        AppSearchSchema organizationSchema =
+                new AppSearchSchema.Builder("Organization")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("notes")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(personSchema, organizationSchema)
+                        .build();
+        mDb1.setSchemaAsync(setSchemaRequest).get();
+
+        Set<AppSearchSchema> actual = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actual).hasSize(2);
+        assertThat(actual).isEqualTo(setSchemaRequest.getSchemas());
+
+        for (AppSearchSchema schema : actual) {
+            if (schema.getSchemaType().equals("Person")) {
+                for (PropertyConfig property : schema.getProperties()) {
+                    if (property.getName().equals("worksFor")) {
+                        assertThat(
+                                ((DocumentPropertyConfig) property)
+                                        .getIndexableNestedProperties()).containsExactly("name");
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
     public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
         AppSearchSchema messageSchema =
@@ -6473,4 +8643,643 @@
         assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
 
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_simple() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+        // Match documents that have embeddings with a similarity closer to 0 that is
+        // greater than -1.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2
+        // - document 1: sum({-0.9}) + sum({}) = -0.9
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_propertyRestriction() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+        // Create a query similar as above but with a property restriction, which still matches
+        // document 0 and document 1 but the semantic score 0.3 should be removed from document 0.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1)
+        // - document 1: -0.9 (embedding1)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5}) = -0.5
+        // - document 1: sum({-0.9}) = -0.9
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "embedding1:semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding1 = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding2)
+        // - document 1: -2.1 (embedding2)
+        EmbeddingVector searchEmbedding2 = new EmbeddingVector(
+                new float[]{-1, -1, 1}, "my_model_v2");
+
+        // Create a complex query that matches all hits from all documents.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2)
+        // - document 1: -0.9 (embedding1), -2.1 (embedding2)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7
+        // - document 1: sum({-0.9}) + sum({-2.1}) = -3
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding1, searchEmbedding2)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0))) + "
+                        + "sum(this.matchedSemanticScores(getSearchSpecEmbedding(1)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0)) OR "
+                        + "semanticSearch(getSearchSpecEmbedding(1))", searchSpec);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_hybrid() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding2)
+        // - document 1: -2.1 (embedding2)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{-1, -1, 1}, "my_model_v2");
+
+        // Create a hybrid query that matches document 0 because of term-based search
+        // and document 1 because of embedding-based search.
+        //
+        // The matched embeddings for each doc are:
+        // - document 1: -2.1 (embedding2)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({}) = 0
+        // - document 1: sum({-2.1}) = -2.1
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "foo OR semanticSearch(getSearchSpecEmbedding(0), -10, -1)", searchSpec);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearchWithoutEnablingFeatureFails() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        ExecutionException executionException = assertThrows(ExecutionException.class,
+                () -> searchResults.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains("EMBEDDING_SEARCH");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_notSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testTokenizeSearch_simple() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .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();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo bar")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1, doc2).build()));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search("tokenize(\"foo.\")", searchSpec);
+        List<GenericDocument> results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc2, doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar, foo\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"\\\"bar, \\\"foo\\\"\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar ) foo\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar foo(\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testTokenizeSearch_notSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("tokenize(\"foo.\")", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled({
+            Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS,
+            Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
+    public void testInformationalRankingExpressions() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        final int doc0DocScore = 2;
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setScore(doc0DocScore)
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding", new EmbeddingVector(
+                                new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+                        .build();
+        final int doc1DocScore = 3;
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setScore(doc1DocScore)
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding", new EmbeddingVector(
+                                        new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"),
+                                new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: 0.5
+        // - document 1: -0.9, 0.5
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model");
+
+        // Make an embedding query that matches all documents.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .addInformationalRankingExpressions(
+                        "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .addInformationalRankingExpressions("this.documentScore()")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0))", searchSpec);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        // doc0:
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5);
+        // doc0 has 1 embedding vector and a document score of 2.
+        assertThat(results.get(0).getInformationalRankingSignals())
+                .containsExactly(1.0, (double) doc0DocScore).inOrder();
+
+        // doc1:
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5);
+        // doc1 has 2 embedding vectors and a document score of 3.
+        assertThat(results.get(1).getInformationalRankingSignals())
+                .containsExactly(2.0, (double) doc1DocScore).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testInformationalRankingExpressions_notSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy("this.documentScore() + 1")
+                .addInformationalRankingExpressions("this.documentScore()")
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("foo", searchSpec));
+        assertThat(exception).hasMessageThat().contains(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                + " are not available on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
+        // 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();
+        mDb1.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(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.getPropertyBytesArray("bytes")).isEmpty();
+        assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
index 453d9225..6f08abf 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionGmsCtsTest.java
@@ -18,24 +18,18 @@
 package androidx.appsearch.cts.app;
 
 import android.content.Context;
-import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.playservicesstorage.PlayServicesStorage;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.Assume;
-import org.junit.Test;
 
 import java.util.concurrent.ExecutorService;
 
-// TODO(b/237116468): Remove SdkSuppress once AppSearchAttributionSource available for lower API
-//  levels.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AppSearchSessionGmsCtsTest extends AppSearchSessionCtsTestBase {
 
     private boolean mIsGmsAvailable;
@@ -68,13 +62,4 @@
             super.tearDown();
         }
     }
-
-    @Override
-    @Test
-    public void testRfc822_unsupportedFeature_throwsException() {
-        // TODO(b/280463238): // TODO(b/280463238): KNOWN_ISSUE will be fixed in next
-        //  play-services-appsearch drop.
-        // expected: tokenizerType is out of range of [0, 1] (too high)
-        // but was : tokenizerType is out of range of [%d, %d] (too high) [0, 1]
-    }
 }
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 aba266a..79e7ad3 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
@@ -20,7 +20,6 @@
 
 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;
 
@@ -34,7 +33,6 @@
 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;
@@ -560,44 +558,4 @@
         assertThat(result.getFailures().get("id1").getErrorMessage())
                 .contains("was too large to write. Max is 16777215");
     }
-
-    @Test
-    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-        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();
-
-        // 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 0fdd512..f2fc0c2 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,9 +16,6 @@
 // @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;
@@ -30,16 +27,9 @@
 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.test.core.app.ApplicationProvider;
 import androidx.test.filters.SdkSuppress;
 
@@ -93,51 +83,11 @@
     }
 
     @Test
+    @Override
     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();
-        }
+        // b/185441119 was fixed in Android T, this test will fail on S_V2 devices and below.
+        assumeTrue(Build.VERSION.SDK_INT >= 33);
+        super.testPutDocuments_emptyBytesAndDocuments();
     }
 
     @Override
@@ -202,5 +152,4 @@
     @Override
     @Test
     public void testQuery_advancedRankingWithJoin() throws Exception { }
-
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
index 1d70497..b666e4d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
@@ -20,10 +20,18 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 
+import org.junit.Rule;
 import org.junit.Test;
 
+import java.util.Objects;
+
 public class GenericDocumentCtsTest {
     private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
@@ -36,6 +44,9 @@
             .setCreationTimestampMillis(6789L)
             .build();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     @SuppressWarnings("deprecation")
     public void testMaxIndexedProperties() {
@@ -364,9 +375,31 @@
                 () -> builder.setPropertyString("testKey", "string1", nullString));
     }
 
-// @exportToFramework:startStrip()
+    @Test
+    public void testDocumentInvalid_setNullByteValues() {
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        byte[] nullBytes = null;
 
-    // TODO(b/171882200): Expose this test in Android T
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyBytes("propBytes", new byte[][]{{1, 2}, nullBytes}));
+    }
+
+    @Test
+    public void testDocumentInvalid_setNullDocValues() {
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        GenericDocument doc = new GenericDocument.Builder<>("namespace",
+                "id2",
+                "schemaType2").build();
+        GenericDocument nullDoc = null;
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyDocument("propDocs", doc, nullDoc));
+    }
+
     @Test
     public void testDocument_toBuilder() {
         GenericDocument document1 = new GenericDocument.Builder<>(
@@ -381,12 +414,13 @@
                 .build();
         GenericDocument document2 =
                 new GenericDocument.Builder<>(document1)
-                .setId("id2")
-                .setNamespace("namespace2")
-                .setPropertyBytes("byteKey1", sByteArray2)
-                .setPropertyLong("longKey2", 10L)
-                .clearProperty("booleanKey1")
-                .build();
+                        .setId("id2")
+                        .setNamespace("namespace2")
+                        .setSchemaType("schemaType2")
+                        .setPropertyBytes("byteKey1", sByteArray2)
+                        .setPropertyLong("longKey2", 10L)
+                        .clearProperty("booleanKey1")
+                        .build();
 
         // Make sure old doc hasn't changed
         assertThat(document1.getId()).isEqualTo("id1");
@@ -399,7 +433,7 @@
 
         // Make sure the new doc contains the expected values
         GenericDocument expectedDoc = new GenericDocument.Builder<>(
-                "namespace2", "id2", "schemaType1")
+                "namespace2", "id2", "schemaType2")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .setPropertyLong("longKey2", 10L)
@@ -411,7 +445,51 @@
         assertThat(document2).isEqualTo(expectedDoc);
     }
 
-// @exportToFramework:endStrip()
+    @Test
+    public void testDocument_toBuilder_doesNotModifyOriginal() {
+        GenericDocument oldDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Hello")
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                                .setPropertyString("propString", "Goodbye")
+                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                                .build())
+                .build();
+
+        GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id3", "schema3")
+                                .setPropertyString("propString", "Bye")
+                                .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
+                                .build())
+                .build();
+
+        // Check that the original GenericDocument is unmodified.
+        assertThat(oldDoc.getScore()).isEqualTo(42);
+        assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Goodbye");
+        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{3, 4}});
+
+        // Check that the new GenericDocument has modified the original fields correctly.
+        assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Bye");
+        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{5, 6}});
+
+        // Check that the new GenericDocument copies fields that aren't set.
+        assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
+        assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
+                "propString"));
+    }
 
     @Test
     public void testRetrieveTopLevelProperties() {
@@ -886,4 +964,259 @@
         assertThat(documents[1].getPropertyNames()).containsExactly("propString", "propInts",
                 "propIntsTwo");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentEquals_identicalWithEmbeddingValues() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentEquals_differentOrderWithEmbeddingValues() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Create second document with same parameter but different order.
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetEmbeddingValue() {
+        EmbeddingVector embedding = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyEmbedding("embeddingKey1", embedding)
+                .build();
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getTtlMillis()).isEqualTo(1L);
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+        assertThat(document.getScore()).isEqualTo(1);
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(Objects.requireNonNull(document.getPropertyEmbedding(
+                "embeddingKey1")).getValues()).usingExactEquality()
+                .containsExactly(1.1f, 2.2f, 3.3f).inOrder();
+        assertThat(Objects.requireNonNull(
+                document.getPropertyEmbedding("embeddingKey1")).getModelSignature()).isEqualTo(
+                "my_model_v1");
+
+        assertThat(document.getProperty("longKey1")).isInstanceOf(long[].class);
+        assertThat((long[]) document.getProperty("longKey1")).asList().containsExactly(1L);
+        assertThat(document.getProperty("doubleKey1")).isInstanceOf(double[].class);
+        assertThat((double[]) document.getProperty("doubleKey1")).usingTolerance(
+                0.05).containsExactly(1.0);
+        assertThat(document.getProperty("booleanKey1")).isInstanceOf(boolean[].class);
+        assertThat((boolean[]) document.getProperty("booleanKey1")).asList().containsExactly(true);
+        assertThat(document.getProperty("stringKey1")).isInstanceOf(String[].class);
+        assertThat((String[]) document.getProperty("stringKey1")).asList().containsExactly(
+                "test-value1");
+        assertThat((EmbeddingVector[]) document.getProperty(
+                "embeddingKey1")).asList().containsExactly(embedding).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetArrayEmbeddingValues() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .build();
+
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0).inOrder();
+        assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
+        assertThat(document.getPropertyEmbeddingArray("embeddingKey1")).asList()
+                .containsExactly(embedding1, embedding2).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocument_setEmptyEmbeddingValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setPropertyBoolean("booleanKey")
+                .setPropertyString("stringKey")
+                .setPropertyBytes("byteKey")
+                .setPropertyDouble("doubleKey")
+                .setPropertyDocument("documentKey")
+                .setPropertyLong("longKey")
+                .setPropertyEmbedding("embeddingKey")
+                .build();
+        assertThat(document.getPropertyBooleanArray("booleanKey")).isEmpty();
+        assertThat(document.getPropertyStringArray("stringKey")).isEmpty();
+        assertThat(document.getPropertyBytesArray("byteKey")).isEmpty();
+        assertThat(document.getPropertyDoubleArray("doubleKey")).isEmpty();
+        assertThat(document.getPropertyDocumentArray("documentKey")).isEmpty();
+        assertThat(document.getPropertyLongArray("longKey")).isEmpty();
+        assertThat(document.getPropertyEmbeddingArray("embeddingKey")).isEmpty();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentInvalid_setNullEmbeddingValues() {
+        EmbeddingVector embedding = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        EmbeddingVector nullEmbedding = null;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setPropertyEmbedding("propEmbeddings",
+                        new EmbeddingVector[]{embedding, nullEmbedding}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocument_toBuilderWithEmbeddingValues() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+        GenericDocument document1 = new GenericDocument.Builder<>(
+                /*namespace=*/"", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyEmbedding("embeddingKey1", embedding1, embedding2)
+                .build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>(document1)
+                        .setId("id2")
+                        .setNamespace("namespace2")
+                        .setPropertyEmbedding("embeddingKey1", embedding2)
+                        .setPropertyLong("longKey2", 10L)
+                        .clearProperty("booleanKey1")
+                        .build();
+
+        // Make sure old doc hasn't changed
+        assertThat(document1.getId()).isEqualTo("id1");
+        assertThat(document1.getNamespace()).isEqualTo("");
+        assertThat(document1.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document1.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document1.getPropertyLongArray("longKey2")).isNull();
+        assertThat(document1.getPropertyEmbeddingArray("embeddingKey1")).asList()
+                .containsExactly(embedding1, embedding2).inOrder();
+
+        // Make sure the new doc contains the expected values
+        GenericDocument expectedDoc = new GenericDocument.Builder<>(
+                "namespace2", "id2", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyLong("longKey2", 10L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyEmbedding("embeddingKey1", embedding2)
+                .build();
+        assertThat(document2).isEqualTo(expectedDoc);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetPropertyNamesWithEmbeddingValue() {
+        EmbeddingVector embedding = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyEmbedding("embeddingKey1", embedding)
+                .build();
+        assertThat(document.getPropertyNames()).containsExactly("longKey1", "doubleKey1",
+                "booleanKey1", "stringKey1", "embeddingKey1");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingValuesCannotBeEmpty() {
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new EmbeddingVector(new float[]{}, "my_model"));
+        assertThat(exception).hasMessageThat().contains("Embedding values cannot be empty.");
+    }
 }
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 405db5a..4d5a4f9 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
@@ -23,6 +23,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 
 import com.google.common.collect.ImmutableSet;
@@ -30,6 +31,7 @@
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.Map;
 
 public class GetSchemaResponseCtsTest {
     @Test
@@ -76,10 +78,10 @@
                         ImmutableSet.of(packageIdentifier2))
                 .setRequiredPermissionsForSchemaTypeVisibility("Email2",
                         ImmutableSet.of(
-                                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
-                                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
-                                        ImmutableSet.of(SetSchemaRequest
-                                                .READ_ASSISTANT_APP_SEARCH_DATA))
+                                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
@@ -156,6 +158,30 @@
                                 .READ_ASSISTANT_APP_SEARCH_DATA)));
     }
 
+
+    @Test
+    public void setVisibilityConfig() {
+        SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg3", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg4", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .setSchemaTypeVisibleToConfigs("Email",
+                                ImmutableSet.of(visibilityConfig1, visibilityConfig2))
+                        .build();
+
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).containsExactly("Email",
+                ImmutableSet.of(visibilityConfig1, visibilityConfig2));
+    }
+
     @Test
     public void getEmptyVisibility() {
         GetSchemaResponse getSchemaResponse =
@@ -166,11 +192,19 @@
         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
     }
 
+    @Test
+    public void getEmptyVisibility_visibilityConfig() {
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .build();
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).isEmpty();
+    }
+
     // @exportToFramework:startStrip()
     // Not exported as setVisibilitySettingSupported is hidden in framework
-     /**
+    /**
      * Makes sure an exception is thrown when visibility getters are called after visibility is set
-      * to no supported.
+     * to no supported.
      */
     @Test
     public void setVisibility_setFalse() {
@@ -209,6 +243,14 @@
                 getSchemaResponse::getRequiredPermissionsForSchemaTypeVisibility);
         assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
                 + " this backend/Android API level combination.");
+        e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getPubliclyVisibleSchemas);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
+        e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getSchemaTypesVisibleToConfigs);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
     }
 
     /**
@@ -248,4 +290,56 @@
                 original::getRequiredPermissionsForSchemaTypeVisibility);
     }
     // @exportToFramework:endStrip()
+
+    @Test
+    public void testVisibility_publicVisibility() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier2)
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                .build();
+        assertThat(getSchemaResponse.getPubliclyVisibleSchemas().get("Email1"))
+                .isEqualTo(packageIdentifier1);
+    }
+
+    // @exportToFramework:startStrip()
+    // Not exported as setVisibilitySettingSupported is hidden in framework
+    @Test
+    public void testVisibility_publicVisibility_clearVisibility() {
+        byte[] sha256cert1 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                // This should clear all visibility settings.
+                .setVisibilitySettingSupported(true)
+                .build();
+
+        Map<String, PackageIdentifier> publiclyVisibleSchemas =
+                getSchemaResponse.getPubliclyVisibleSchemas();
+        assertThat(publiclyVisibleSchemas).isEmpty();
+    }
+
+    @Test
+    public void testVisibility_publicVisibility_notSupported() {
+        byte[] sha256cert1 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                .setVisibilitySettingSupported(false)
+                .build();
+
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getPubliclyVisibleSchemas);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
+    }
+    // @exportToFramework:endStrip()
 }
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 9f38b9c..7df269c 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
@@ -751,7 +751,7 @@
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4);
 
@@ -761,7 +761,7 @@
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4, inEmail3);
 
@@ -772,7 +772,7 @@
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
                                 SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4, inEmail3);
     }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
index 40d97ef..6e77eae 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionGmsCtsTest.java
@@ -18,23 +18,18 @@
 package androidx.appsearch.cts.app;
 
 import android.content.Context;
-import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.playservicesstorage.PlayServicesStorage;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.Assume;
 import org.junit.Ignore;
 
-// TODO(b/237116468): Remove SdkSuppress once AppSearchAttributionSource available for lower API
-//  levels.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class GlobalSearchSessionGmsCtsTest extends GlobalSearchSessionCtsTestBase {
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private boolean mIsGmsAvailable;
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 c2b06d4..a50f7fc 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
@@ -20,14 +20,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.content.Context;
 
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableSet;
@@ -50,7 +56,99 @@
         assertThat(request.getGenericDocuments().get(1).getId()).isEqualTo("test2");
     }
 
-// @exportToFramework:startStrip()
+    @Test
+    public void duplicateIdForNormalAndTakenActionGenericDocumentThrowsException()
+            throws Exception {
+        GenericDocument normalDocument = new GenericDocument.Builder<>(
+                "namespace", "id", "builtin:Thing").build();
+        GenericDocument takenActionGenericDocument = new GenericDocument.Builder<>(
+                "namespace", "id", "builtin:ClickAction").build();
+
+        PutDocumentsRequest.Builder builder = new PutDocumentsRequest.Builder()
+                .addGenericDocuments(normalDocument)
+                .addTakenActionGenericDocuments(takenActionGenericDocument);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> builder.build());
+        assertThat(e.getMessage()).isEqualTo("Document id " + takenActionGenericDocument.getId()
+                + " cannot exist in both taken action and normal document");
+    }
+
+    @Test
+    public void addTakenActionGenericDocuments() throws Exception {
+        GenericDocument searchActionGenericDocument1 = new GenericDocument.Builder<>(
+                "namespace", "search1", "builtin:SearchAction").build();
+        GenericDocument clickActionGenericDocument1 = new GenericDocument.Builder<>(
+                "namespace", "click1", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument2 = new GenericDocument.Builder<>(
+                "namespace", "click2", "builtin:ClickAction").build();
+        GenericDocument searchActionGenericDocument2 = new GenericDocument.Builder<>(
+                "namespace", "search2", "builtin:SearchAction").build();
+        GenericDocument clickActionGenericDocument3 = new GenericDocument.Builder<>(
+                "namespace", "click3", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument4 = new GenericDocument.Builder<>(
+                "namespace", "click4", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument5 = new GenericDocument.Builder<>(
+                "namespace", "click5", "builtin:ClickAction").build();
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActionGenericDocuments(
+                        searchActionGenericDocument1, clickActionGenericDocument1,
+                        clickActionGenericDocument2, searchActionGenericDocument2,
+                        clickActionGenericDocument3, clickActionGenericDocument4,
+                        clickActionGenericDocument5)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    @Test
+    public void addTakenActionGenericDocuments_byCollection() throws Exception {
+        Set<GenericDocument> takenActionGenericDocuments = ImmutableSet.of(
+                new GenericDocument.Builder<>(
+                        "namespace", "search1", "builtin:SearchAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click1", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click2", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "search2", "builtin:SearchAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click3", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click4", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click5", "builtin:ClickAction").build());
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActionGenericDocuments(takenActionGenericDocuments)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    // @exportToFramework:startStrip()
     @Document
     static class Card {
         @Document.Namespace
@@ -87,5 +185,84 @@
 
         assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("cardId");
     }
+
+    @Test
+    public void addTakenActions() throws Exception {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .build();
+        SearchAction searchAction2 =
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+                        .build();
+        ClickAction clickAction4 =
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+                        .build();
+        ClickAction clickAction5 =
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+                        .build();
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActions(searchAction1, clickAction1, clickAction2, searchAction2,
+                        clickAction3, clickAction4, clickAction5)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    @Test
+    public void addTakenActions_byCollection() throws Exception {
+        Set<TakenAction> takenActions = ImmutableSet.of(
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .build(),
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+                        .build());
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActions(takenActions)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
 // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
new file mode 100644
index 0000000..6787a56f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class SchemaVisibilityConfigCtsTest {
+
+    @Test
+    public void testBuildVisibilityConfig() {
+        byte[] cert1 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        byte[] cert2 = new byte[32];
+        Arrays.fill(cert2, (byte) 2);
+        SchemaVisibilityConfig schemaVisibilityConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", cert1))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        assertThat(schemaVisibilityConfig.getRequiredPermissions())
+                .containsExactly(ImmutableSet.of(1, 2));
+        assertThat(schemaVisibilityConfig.getAllowedPackages())
+                .containsExactly(new PackageIdentifier("pkg1", cert1));
+        assertThat(schemaVisibilityConfig.getPubliclyVisibleTargetPackage())
+                .isEqualTo(new PackageIdentifier("pkg2", cert2));
+    }
+
+    @Test
+    public void testVisibilityConfigEquals() {
+        // Create two VisibilityConfig instances with the same properties
+        SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        // Test equals method
+        assertThat(visibilityConfig1).isEqualTo(visibilityConfig2);
+        assertThat(visibilityConfig2).isEqualTo(visibilityConfig1);
+    }
+
+    @Test
+    public void testVisibilityConfig_rebuild() {
+        String visibleToPackage = "com.example.package";
+        byte[] visibleToPackageCert = new byte[32];
+
+        String publiclyVisibleTarget = "com.example.test";
+        byte[] publiclyVisibleTargetCert = new byte[32];
+
+        SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier(visibleToPackage, visibleToPackageCert))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier(
+                        publiclyVisibleTarget, publiclyVisibleTargetCert))
+                .addRequiredPermissions(ImmutableSet.of(1, 2));
+
+        // Create a VisibilityConfig using the Builder
+        SchemaVisibilityConfig original = builder.build();
+
+        SchemaVisibilityConfig rebuild = builder.clearAllowedPackages()
+                .setPubliclyVisibleTargetPackage(null)
+                .clearRequiredPermissions().build();
+
+        // Check if the properties are set correctly
+        assertThat(original.getAllowedPackages()).containsExactly(
+                new PackageIdentifier(visibleToPackage, visibleToPackageCert));
+        assertThat(original.getPubliclyVisibleTargetPackage()).isEqualTo(
+                new PackageIdentifier(publiclyVisibleTarget, publiclyVisibleTargetCert));
+        assertThat(original.getRequiredPermissions()).containsExactly(ImmutableSet.of(1, 2));
+
+        assertThat(rebuild.getAllowedPackages()).isEmpty();
+        assertThat(rebuild.getPubliclyVisibleTargetPackage()).isNull();
+        assertThat(rebuild.getRequiredPermissions()).isEmpty();
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
index 426d01a..382d5272 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -22,11 +22,18 @@
 
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 public class SearchResultCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Test
     public void testBuildSearchResult() {
@@ -172,4 +179,48 @@
         SearchResult rebuildJoinedResult2 = rebuild.getJoinedResults().get(1);
         assertThat(rebuildJoinedResult2.getGenericDocument().getId()).isEqualTo("id3");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testBuildSearchResult_informationalRankingSignals() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+        SearchResult searchResult = new SearchResult.Builder("packageName", "databaseName")
+                .setGenericDocument(email)
+                .setRankingSignal(2.9)
+                .addInformationalRankingSignal(3.0)
+                .addInformationalRankingSignal(4.0)
+                .build();
+
+        assertThat(searchResult.getRankingSignal()).isEqualTo(2.9);
+        assertThat(searchResult.getInformationalRankingSignals())
+                .containsExactly(3.0, 4.0).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testRebuild_informationalRankingSignals() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+
+        SearchResult.Builder searchResultBuilder =
+                new SearchResult.Builder("packageName", "databaseName")
+                        .setGenericDocument(email)
+                        .setRankingSignal(2.9)
+                        .addInformationalRankingSignal(3.0)
+                        .addInformationalRankingSignal(4.0);
+
+        SearchResult original = searchResultBuilder.build();
+        SearchResult rebuild = searchResultBuilder.addInformationalRankingSignal(5).build();
+
+        // Rebuild won't effect the original object
+        assertThat(original.getRankingSignal()).isEqualTo(2.9);
+        assertThat(original.getInformationalRankingSignals()).containsExactly(3.0, 4.0).inOrder();
+
+        assertThat(rebuild.getRankingSignal()).isEqualTo(2.9);
+        assertThat(rebuild.getInformationalRankingSignals())
+                .containsExactly(3.0, 4.0, 5.0).inOrder();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index f465eaf..a6820fb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -24,14 +24,20 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Collections;
@@ -40,6 +46,9 @@
 import java.util.Set;
 
 public class SearchSpecCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void testBuildSearchSpecWithoutTermMatch() {
         SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("testSchemaType").build();
@@ -118,6 +127,52 @@
     }
 
     @Test
+    public void testBuildSearchSpec_searchSourceLogTag() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setSearchSourceLogTag("logTag")
+                .build();
+
+        assertThat(searchSpec.getSearchSourceLogTag()).isEqualTo("logTag");
+    }
+
+    @Test
+    public void testBuildSearchSpec_searchSourceLogTag_exceedLengthLimitation() {
+        String longTag = new String(new char[110]);
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new SearchSpec.Builder()
+                        .setSearchSourceLogTag(longTag));
+        assertThat(e).hasMessageThat().contains(
+                "The maximum supported tag length is 100. This tag is too long");
+    }
+
+    @Test
+    public void testBuildSearchSpec_searchSourceLogTag_defaultIsNull() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .build();
+
+        assertThat(searchSpec.getSearchSourceLogTag()).isNull();
+    }
+
+    // TODO(b/309826655): Flag guard this test.
+    @Test
+    public void testBuildSearchSpec_hasProperty() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+
+        assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+        assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+    }
+
+    @Test
     public void testGetProjectionTypePropertyMasks() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -404,6 +459,24 @@
         assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
     }
 
+    // TODO(b/309826655): Flag guard this test.
+    @Test
+    public void testSetFeatureEnabledToFalse_hasProperty() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterQueryLanguageEnabled(false)
+                .setListFilterHasPropertyFunctionEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isFalse();
+    }
 
     @Test
     public void testInvalidAdvancedRanking() {
@@ -463,20 +536,6 @@
     }
 
     @Test
-    public void testProjections_withSchemaFilter() throws Exception {
-        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterSchemas("Filter")
-                .addProjectionPathsForDocumentClass(King.class, ImmutableList.of(
-                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
-
-        IllegalArgumentException exception =
-                assertThrows(IllegalArgumentException.class, searchSpecBuilder::build);
-        assertThat(exception.getMessage())
-                .isEqualTo("Projection requested for schema not in schemas filters: King");
-    }
-
-    @Test
     public void testTypePropertyWeightsForDocumentClass() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -553,6 +612,40 @@
     }
 
     @Test
+    public void testGetPropertyFiltersTypePropertyMasks() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+                .addFilterProperties("TypeB", ImmutableList.of("field7"))
+                .addFilterProperties("TypeC", ImmutableList.of())
+                .build();
+
+        Map<String, List<String>> typePropertyPathMap = searchSpec.getFilterProperties();
+        assertThat(typePropertyPathMap.keySet())
+                .containsExactly("TypeA", "TypeB", "TypeC");
+        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
+        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
+        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+    }
+
+    @Test
+    public void testFilterSchemas_wildcardProjection() {
+        // Should not crash
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("ParentType")
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.singletonList("TypeA"))
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        Collections.singletonList("TypeB"))
+                .build();
+
+        assertThat(searchSpec.getFilterSchemas()).containsExactly("ParentType");
+        assertThat(searchSpec.getProjections())
+                .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeA"));
+        assertThat(searchSpec.getFilterProperties())
+                .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeB"));
+    }
+
+    @Test
     public void testRebuild() {
         JoinSpec originalJoinSpec = new JoinSpec.Builder("entityId")
                 .setNestedSearch("joe", new SearchSpec.Builder().addFilterSchemas("Action").build())
@@ -583,4 +676,163 @@
         assertThat(rebuild.getJoinSpec().getNestedSearchSpec().getFilterSchemas())
                 .containsExactly("CallAction");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(embedding1, embedding2)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testRebuild_embeddingSearch() {
+        EmbeddingVector embedding1 = new EmbeddingVector(
+                new float[]{1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 = new EmbeddingVector(
+                new float[]{4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+
+        // Create a builder
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(embedding1);
+        SearchSpec searchSpec1 = searchSpecBuilder.build();
+
+        // Add a new embedding to the builder and rebuild. We should see that the new embedding
+        // is only added to searchSpec2.
+        searchSpecBuilder.addSearchEmbeddings(embedding2);
+        SearchSpec searchSpec2 = searchSpecBuilder.build();
+
+        assertThat(searchSpec1.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec1.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec1.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec1.getSearchEmbeddings()).containsExactly(embedding1);
+
+        assertThat(searchSpec2.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec2.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec2.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec2.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testBuildSearchSpec_embeddingSearch() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+
+        assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+        assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testSetFeatureEnabledToFalse_embeddingSearch() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterQueryLanguageEnabled(false)
+                .setEmbeddingSearchEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testListFilterTokenizeFunction() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testSetFeatureEnabledToFalse_tokenizeFunction() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterTokenizeFunctionEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testInformationalRankingExpressions() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setOrder(SearchSpec.ORDER_ASCENDING)
+                .setRankingStrategy("this.documentScore()")
+                .addInformationalRankingExpressions("this.relevanceScore()")
+                .build();
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
+                .isEqualTo(SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION);
+        assertThat(searchSpec.getAdvancedRankingExpression())
+                .isEqualTo("this.documentScore()");
+        assertThat(searchSpec.getInformationalRankingExpressions()).containsExactly(
+                "this.relevanceScore()");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testRebuild_informationalRankingExpressions() {
+        SearchSpec.Builder searchSpecBuilder =
+                new SearchSpec.Builder().addInformationalRankingExpressions(
+                        "this.relevanceScore()");
+
+        SearchSpec original = searchSpecBuilder.build();
+        SearchSpec rebuild = searchSpecBuilder
+                .addInformationalRankingExpressions("this.documentScore()")
+                .build();
+
+        // Rebuild won't effect the original object
+        assertThat(original.getInformationalRankingExpressions())
+                .containsExactly("this.relevanceScore()");
+
+        assertThat(rebuild.getInformationalRankingExpressions())
+                .containsExactly("this.relevanceScore()", "this.documentScore()").inOrder();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
index 69979c3..6f1e79b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSuggestionSpec;
 
 import com.google.common.collect.ImmutableList;
@@ -75,6 +76,38 @@
     }
 
     @Test
+    public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
+        SearchSuggestionSpec searchSuggestionSpec =
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .setRankingStrategy(SearchSuggestionSpec
+                                .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
+                        .addFilterSchemas("Person", "Email")
+                        .addFilterSchemas(ImmutableList.of("Foo"))
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+                        .addFilterPropertyPaths("Foo",
+                                ImmutableList.of(new PropertyPath("Bar")))
+                        .build();
+
+        assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
+        assertThat(searchSuggestionSpec.getFilterSchemas())
+                .containsExactly("Person", "Email", "Foo");
+        assertThat(searchSuggestionSpec.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
+                        "Foo",  ImmutableList.of("Bar"));
+    }
+
+    @Test
+    public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .addFilterSchemas("Person")
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+                        .build());
+        assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
+                + "property filter but doesn't exist in the schema filter.");
+    }
+
+    @Test
     public void testRebuild() throws Exception {
         SearchSuggestionSpec.Builder builder =
                 new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
@@ -106,4 +139,31 @@
         assertThat(rebuild.getFilterSchemas())
                 .containsExactly("Person", "Email", "Message", "Foo");
     }
+
+    @Test
+    public void testRebuild_withPropertyFilter() throws Exception {
+        SearchSuggestionSpec.Builder builder =
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .addFilterSchemas("Person", "Email")
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
+
+        SearchSuggestionSpec original = builder.build();
+
+        builder.addFilterSchemas("Message", "Foo")
+                .addFilterProperties("Foo", ImmutableList.of("Bar"));
+        SearchSuggestionSpec rebuild = builder.build();
+
+        assertThat(original.getMaximumResultCount()).isEqualTo(123);
+        assertThat(original.getFilterSchemas())
+                .containsExactly("Person", "Email");
+        assertThat(original.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"));
+
+        assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
+        assertThat(rebuild.getFilterSchemas())
+                .containsExactly("Person", "Email", "Message", "Foo");
+        assertThat(rebuild.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
+                        "Foo",  ImmutableList.of("Bar"));
+    }
 }
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 d7de450..1876b87 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
@@ -30,6 +30,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.testutil.AppSearchEmail;
@@ -201,6 +202,32 @@
     }
 
     @Test
+    public void testInvalidSchemaReferences_fromPubliclyVisible() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setPubliclyVisibleSchema("InvalidSchema",
+                        new PackageIdentifier("com.foo.package",
+                                /*sha256Certificate=*/ new byte[]{})).build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testInvalidSchemaReferences_fromVisibleToConfigs() {
+        byte[] sha256cert1 = new byte[32];
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder()
+                        .addSchemaTypeVisibleToConfig("InvalidSchema", config)
+                        .build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
     public void testSetSchemaTypeDisplayedBySystem_displayed() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
@@ -276,7 +303,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Clear the permissions in the builder
         setSchemaRequestBuilder.clearRequiredPermissionsForSchemaTypeVisibility("Schema1");
@@ -287,7 +314,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Old object should remain unchanged
         assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -300,7 +327,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
     }
 
     @Test
@@ -375,6 +402,132 @@
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
     }
 
+    @Test
+    public void testPubliclyVisibleSchemaType() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+    }
+
+    @Test
+    public void testPubliclyVisibleSchemaType_removal() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+
+        // Removed Schema
+        request = new SetSchemaRequest.Builder().addSchemas(schema)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier)
+                .setPubliclyVisibleSchema("Schema", null)
+                .build();
+        assertThat(request.getPubliclyVisibleSchemas()).isEmpty();
+    }
+
+    @Test
+    public void testPubliclyVisibleSchemaType_deduped() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        PackageIdentifier packageIdentifier2 =
+                new PackageIdentifier("com.package.bar", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+
+        // Deduped schema
+        request = new SetSchemaRequest.Builder().addSchemas(schema)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier2)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier)
+                .build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+    }
+
+    @Test
+    public void testSetSchemaTypeVisibleForConfigs() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .addSchemaTypeVisibleToConfig("Schema", config1)
+                .addSchemaTypeVisibleToConfig("Schema", config2)
+                .build();
+
+        assertThat(request.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+    }
+
+    @Test
+    public void testClearSchemaTypeVisibleForConfigs() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .addSchemaTypeVisibleToConfig("Schema", config1)
+                .addSchemaTypeVisibleToConfig("Schema", config2);
+
+        SetSchemaRequest original = builder.build();
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+
+        builder.clearSchemaTypeVisibleToConfigs("Schema");
+        SetSchemaRequest rebuild = builder.build();
+
+        // rebuild has empty visible to configs
+        assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+        // original keep in the same state
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+    }
 
     // @exportToFramework:startStrip()
     @Document
@@ -611,7 +764,7 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Clear the permissions in the builder
         setSchemaRequestBuilder.clearRequiredPermissionsForDocumentClassVisibility(King.class);
@@ -622,7 +775,7 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Old object should remain unchanged
         assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -635,7 +788,76 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
+    }
+
+    @Test
+    public void testSetDocumentClassVisibleForConfigs() throws Exception {
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addDocumentClasses(King.class, Queen.class)
+                .addDocumentClassVisibleToConfig(King.class, config1)
+                .addDocumentClassVisibleToConfig(King.class, config2)
+                .build();
+
+        assertThat(request.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
+    }
+
+    @Test
+    public void testClearDocumentClassVisibleForConfigs() throws Exception {
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addDocumentClasses(King.class, Queen.class)
+                .addDocumentClassVisibleToConfig(King.class, config1)
+                .addDocumentClassVisibleToConfig(King.class, config2);
+
+        SetSchemaRequest original = builder.build();
+
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
+
+        // Clear the visbleToConfigs
+        builder.clearDocumentClassVisibleToConfigs(King.class);
+        SetSchemaRequest rebuild = builder.build();
+
+        // rebuild object has empty visibleToConfigs
+        assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+        // original keep in same state.
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
     }
 // @exportToFramework:endStrip()
 
@@ -740,6 +962,91 @@
     }
 
     @Test
+    public void testRebuild_visibleConfigs() {
+        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);
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        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)
+                .addSchemaTypeVisibleToConfig("Email1", config1);
+
+        SetSchemaRequest original = builder.build();
+        SetSchemaRequest rebuild = builder
+                .addSchemas(schema2)
+                .addSchemaTypeVisibleToConfig("Email2", config2)
+                .build();
+
+        assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly(
+                "Email1", ImmutableSet.of(config1));
+
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(rebuild.getSchemasVisibleToConfigs()).containsExactly(
+                "Email1", ImmutableSet.of(config1),
+                "Email2", ImmutableSet.of(config2));
+    }
+
+    @Test
+    public void testSetVisibility_publicVisibility_rebuild() {
+        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").build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2").build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema1).setPubliclyVisibleSchema("Email1", packageIdentifier1);
+
+        SetSchemaRequest original = builder.build();
+        SetSchemaRequest rebuild = builder.addSchemas(schema2)
+                .setPubliclyVisibleSchema("Email2", packageIdentifier2).build();
+
+        assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getPubliclyVisibleSchemas())
+                .containsExactly("Email1", packageIdentifier1);
+
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(original.getPubliclyVisibleSchemas())
+                .containsExactly("Email1", packageIdentifier1);
+    }
+
+    @Test
     public void getAndModify() {
         byte[] sha256cert1 = new byte[32];
         byte[] sha256cert2 = new byte[32];
@@ -769,7 +1076,8 @@
                 .build();
 
         // get the visibility setting and modify the output object.
-        // skip getSchemasNotDisplayedBySystem since it returns an unmodifiable object.
+        // skip getSchemasNotDisplayedBySystem and getPubliclyVisibleSchemas since they return
+        // unmodifiable objects.
         request.getSchemasVisibleToPackages().put("Email2", ImmutableSet.of(packageIdentifier2));
         request.getRequiredPermissionsForSchemaTypeVisibility().put("Email2",
                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CALENDAR)));
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
index 06f1bd4..55840c6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
@@ -27,17 +27,17 @@
 public class DocumentChangeInfoCtsTest {
     @Test
     public void testConstructor() {
-        DocumentChangeInfo DocumentChangeInfo = new DocumentChangeInfo(
+        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())
+        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");
     }
 
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
new file mode 100644
index 0000000..48f7846
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class ClickActionCtsTest {
+    @Test
+    public void testBuilder() {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+
+        assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction.getId()).isEqualTo("id");
+        assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction.getQuery()).isEqualTo("query");
+        assertThat(clickAction.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+        assertThat(clickAction.getResultRankInBlock()).isEqualTo(1);
+        assertThat(clickAction.getResultRankGlobal()).isEqualTo(3);
+        assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(65536);
+    }
+
+    @Test
+    public void testBuilder_defaultValues() {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .build();
+
+        assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction.getId()).isEqualTo("id");
+        assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction.getDocumentTtlMillis())
+                .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+        assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction.getQuery()).isNull();
+        assertThat(clickAction.getReferencedQualifiedId()).isNull();
+        assertThat(clickAction.getResultRankInBlock()).isEqualTo(-1);
+        assertThat(clickAction.getResultRankGlobal()).isEqualTo(-1);
+        assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+        ClickAction clickAction2 = new ClickAction.Builder(clickAction1).build();
+
+        // All fields should be copied correctly from clickAction1 to the builder and propagates to
+        // clickAction2 after calling build().
+        assertThat(clickAction2.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction2.getId()).isEqualTo("id");
+        assertThat(clickAction2.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction2.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(clickAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction2.getQuery()).isEqualTo("query");
+        assertThat(clickAction2.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+        assertThat(clickAction2.getResultRankInBlock()).isEqualTo(1);
+        assertThat(clickAction2.getResultRankGlobal()).isEqualTo(3);
+        assertThat(clickAction2.getTimeStayOnResultMillis()).isEqualTo(65536);
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+
+        GenericDocument document = GenericDocument.fromDocumentClass(clickAction);
+        assertThat(document.getNamespace()).isEqualTo("namespace");
+        assertThat(document.getId()).isEqualTo("id");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+        assertThat(document.getTtlMillis()).isEqualTo(456);
+        assertThat(document.getPropertyLong("actionType"))
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(document.getPropertyString("query")).isEqualTo("query");
+        assertThat(document.getPropertyString("referencedQualifiedId"))
+                .isEqualTo("pkg$db/ns#refId");
+        assertThat(document.getPropertyLong("resultRankInBlock")).isEqualTo(1);
+        assertThat(document.getPropertyLong("resultRankGlobal")).isEqualTo(3);
+        assertThat(document.getPropertyLong("timeStayOnResultMillis")).isEqualTo(65536);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
new file mode 100644
index 0000000..a4a63a6
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class SearchActionCtsTest {
+    @Test
+    public void testBuilder() {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                    .setDocumentTtlMillis(456)
+                    .setQuery("query")
+                    .setFetchedResultCount(1)
+                    .build();
+
+        assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction.getId()).isEqualTo("id");
+        assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction.getQuery()).isEqualTo("query");
+        assertThat(searchAction.getFetchedResultCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testBuilder_defaultValues() {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .build();
+
+        assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction.getId()).isEqualTo("id");
+        assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction.getDocumentTtlMillis())
+                .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+        assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction.getQuery()).isNull();
+        assertThat(searchAction.getFetchedResultCount()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setFetchedResultCount(1)
+                        .build();
+        SearchAction searchAction2 = new SearchAction.Builder(searchAction1).build();
+
+        assertThat(searchAction2.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction2.getId()).isEqualTo("id");
+        assertThat(searchAction2.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction2.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(searchAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction2.getQuery()).isEqualTo("query");
+        assertThat(searchAction2.getFetchedResultCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                    .setDocumentTtlMillis(456)
+                    .setQuery("query")
+                    .setFetchedResultCount(1)
+                    .build();
+
+        GenericDocument document = GenericDocument.fromDocumentClass(searchAction);
+        assertThat(document.getNamespace()).isEqualTo("namespace");
+        assertThat(document.getId()).isEqualTo("id");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+        assertThat(document.getTtlMillis()).isEqualTo(456);
+        assertThat(document.getPropertyLong("actionType"))
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(document.getPropertyString("query")).isEqualTo("query");
+        assertThat(document.getPropertyLong("fetchedResultCount")).isEqualTo(1);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
new file mode 100644
index 0000000..3ec5386
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Shim for real CheckFlagsRule defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */
+public final class CheckFlagsRule implements TestRule {
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return base;
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
new file mode 100644
index 0000000..1a2858a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+/**
+ * Shim for real DeviceFlagsValueProvider defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ */
+public final class DeviceFlagsValueProvider {
+    private DeviceFlagsValueProvider() {}
+
+    /** Provides a shim rule that can be used to check the status of flags on device */
+    public static CheckFlagsRule createCheckFlagsRule() {
+        return new CheckFlagsRule();
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
new file mode 100644
index 0000000..14169d7
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class FlagsTest {
+    @Test
+    public void testFlagValue_enableSafeParcelable2() {
+        assertThat(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2).isEqualTo(
+                "com.android.appsearch.flags.enable_safe_parcelable_2");
+    }
+
+    @Test
+    public void testFlagValue_enableListFilterHasPropertyFunction() {
+        assertThat(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION).isEqualTo(
+                "com.android.appsearch.flags.enable_list_filter_has_property_function");
+    }
+
+    @Test
+    public void testFlagValue_enableGroupingTypePerSchema() {
+        assertThat(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA).isEqualTo(
+                "com.android.appsearch.flags.enable_grouping_type_per_schema");
+    }
+
+    @Test
+    public void testFlagValue_enableGenericDocumentCopyConstructor() {
+        assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR).isEqualTo("com.android"
+                + ".appsearch.flags.enable_generic_document_copy_constructor");
+    }
+
+    @Test
+    public void testFlagValue_enableSearchSpecFilterProperties() {
+        assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES).isEqualTo(
+                "com.android.appsearch.flags.enable_search_spec_filter_properties");
+    }
+
+    @Test
+    public void testFlagValue_enableSearchSpecSetSearchSourceLogTag() {
+        assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG).isEqualTo(
+                "com.android.appsearch.flags.enable_search_spec_set_search_source_log_tag");
+    }
+
+    @Test
+    public void testFlagValue_enableSetSchemaVisibleToConfigs() {
+        assertThat(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS).isEqualTo("com"
+                + ".android.appsearch.flags.enable_set_schema_visible_to_configs");
+    }
+
+    @Test
+    public void testFlagValue_enablePutDocumentsRequestAddTakenActions() {
+        assertThat(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS).isEqualTo(
+                "com.android.appsearch.flags.enable_put_documents_request_add_taken_actions");
+    }
+
+    @Test
+    public void testFlagValue_enableGenericDocumentBuilderHiddenMethods() {
+        assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS).isEqualTo("com"
+                + ".android.appsearch.flags.enable_generic_document_builder_hidden_methods");
+    }
+
+    @Test
+    public void testFlagValue_enableSetPubliclyVisibleSchema() {
+        assertThat(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+                .isEqualTo(
+                        "com.android.appsearch.flags.enable_set_publicly_visible_schema");
+    }
+
+    @Test
+    public void testFlagValue_enableEnterpriseGlobalSearchSession() {
+        assertThat(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
+                .isEqualTo("com.android.appsearch.flags.enable_enterprise_global_search_session");
+    }
+
+    @Test
+    public void testFlagValue_enableResultDeniedAndResultRateLimited() {
+        assertThat(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
+                .isEqualTo(
+                        "com.android.appsearch.flags.enable_result_denied_and_result_rate_limited");
+    }
+
+    @Test
+    public void testFlagValue_enableGetParentTypesAndIndexableNestedProperties() {
+        assertThat(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
+                .isEqualTo(
+                        "com.android.appsearch.flags"
+                                + ".enable_get_parent_types_and_indexable_nested_properties");
+    }
+
+    @Test
+    public void testFlagValue_enableSchemaEmbeddingPropertyConfig() {
+        assertThat(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+                .isEqualTo("com.android.appsearch.flags.enable_schema_embedding_property_config");
+    }
+
+    @Test
+    public void testFlagValue_enableListFilterTokenizeFunction() {
+        assertThat(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+                .isEqualTo("com.android.appsearch.flags.enable_list_filter_tokenize_function");
+    }
+
+    @Test
+    public void testFlagValue_enableInformationalRankingExpressions() {
+        assertThat(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+                .isEqualTo("com.android.appsearch.flags.enable_informational_ranking_expressions");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
new file mode 100644
index 0000000..d4f3bf0
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Shim for real RequiresFlagsEnabled defined in Framework.
+ *
+ * <p>In Jetpack, this shim does nothing and exists only for code sync purpose.
+ *
+ * <p>In Framework, indicates that a specific test or class should be run only if all of the given
+ * feature flags are enabled in the device's current state. Enforced by the {@code CheckFlagsRule}.
+ *
+ * <p>This annotation works together with RequiresFlagsDisabled to define the value that is
+ * required of the flag by the test for the test to run. It is an error for either a method or class
+ * to require that a particular flag be both enabled and disabled.
+ *
+ * <p>If the value of a particular flag is required (by either {@code RequiresFlagsEnabled} or
+ * {@code RequiresFlagsDisabled}) by both the class and test method, then the values must be
+ * consistent.
+ *
+ * <p>If the value of a one flag is required by an annotation on the class, and the value of a
+ * different flag is required by an annotation of the method, then both requirements apply.
+ *
+ * <p>With {@code CheckFlagsRule}, test(s) will be skipped with 'assumption failed' when any of the
+ * required flag on the target Android platform is disabled.
+ *
+ * <p>Both {@code SetFlagsRule} and {@code CheckFlagsRule} will fail the test if a particular flag
+ * is both set (with {@code EnableFlags} or {@code DisableFlags}) and required (with {@code
+ * RequiresFlagsEnabled} or {@code RequiresFlagsDisabled}).
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresFlagsEnabled {
+    /**
+     * The list of the feature flags that require to be enabled. Each item is the full flag name
+     * with the format {package_name}.{flag_name}.
+     */
+    String[] value();
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
index d8cc9bc..0900ec8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
@@ -20,9 +20,15 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Parcel;
+
+import androidx.appsearch.app.EmbeddingVector;
+
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 /** Tests for {@link androidx.appsearch.app.GenericDocument} related SafeParcels. */
@@ -34,6 +40,8 @@
         double[] doubleValues = {1.0, 2.0};
         boolean[] booleanValues = {true, false};
         byte[][] bytesValues = {new byte[1]};
+        EmbeddingVector[] embeddingValues = {new EmbeddingVector(new float[1],
+                "my_model")};
         GenericDocumentParcel[] docValues = {(new GenericDocumentParcel.Builder(
                 "namespace", "id", "schemaType")).build()};
 
@@ -52,6 +60,9 @@
         assertThat(new PropertyParcel.Builder("name").setBytesValues(
                 bytesValues).build().getBytesValues()).isEqualTo(
                 Arrays.copyOf(bytesValues, bytesValues.length));
+        assertThat(new PropertyParcel.Builder("name").setEmbeddingValues(
+                embeddingValues).build().getEmbeddingValues()).isEqualTo(
+                Arrays.copyOf(embeddingValues, embeddingValues.length));
         assertThat(new PropertyParcel.Builder("name").setDocumentValues(
                 docValues).build().getDocumentValues()).isEqualTo(
                 Arrays.copyOf(docValues, docValues.length));
@@ -85,14 +96,14 @@
         builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
-        PropertyParcel[] properties = genericDocumentParcel.getProperties();
+        List<PropertyParcel> properties = genericDocumentParcel.getProperties();
         Map<String, PropertyParcel> propertyMap = genericDocumentParcel.getPropertyMap();
         PropertyParcel longArrayProperty = new PropertyParcel.Builder(
                 /*name=*/ "longArray").setLongValues(longArray).build();
         PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
                 /*name=*/ "stringArray").setStringValues(stringArray).build();
 
-        assertThat(properties).asList().containsExactly(longArrayProperty, stringArrayProperty);
+        assertThat(properties).containsExactly(longArrayProperty, stringArrayProperty);
         assertThat(propertyMap).containsExactly("longArray", longArrayProperty,
                 "stringArray", stringArrayProperty);
     }
@@ -106,8 +117,10 @@
                         /*schemaType=*/ "schemaType");
         long[] longArray = new long[]{1L, 2L, 3L};
         String[] stringArray = new String[]{"hello", "world", "!"};
+        List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
         builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
         builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.setParentTypes(parentTypes);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
         GenericDocumentParcel genericDocumentParcelCopy =
@@ -124,9 +137,130 @@
                 genericDocumentParcel.getTtlMillis());
         assertThat(genericDocumentParcelCopy.getScore()).isEqualTo(
                 genericDocumentParcel.getScore());
+        assertThat(genericDocumentParcelCopy.getParentTypes()).isEqualTo(
+                genericDocumentParcel.getParentTypes());
+
         // Check it is a copy.
         assertThat(genericDocumentParcelCopy).isNotSameInstanceAs(genericDocumentParcel);
         assertThat(genericDocumentParcelCopy.getProperties()).isEqualTo(
                 genericDocumentParcel.getProperties());
     }
+
+    @Test
+    public void testGenericDocumentParcelWithParentTypes() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+
+        builder.setParentTypes(parentTypes);
+        GenericDocumentParcel genericDocumentParcel = builder.build();
+
+        assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+    }
+
+    @Test
+    public void testGenericDocumentParcel_builderCanBeReused() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        long[] longArray = new long[]{1L, 2L, 3L};
+        String[] stringArray = new String[]{"hello", "world", "!"};
+        List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
+        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.setParentTypes(parentTypes);
+
+        GenericDocumentParcel genericDocumentParcel = builder.build();
+        builder.setParentTypes(new ArrayList<>(Arrays.asList("parentType3", "parentType4")));
+        builder.clearProperty("longArray");
+        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ new String[]{""});
+
+        PropertyParcel longArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "longArray").setLongValues(longArray).build();
+        PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "stringArray").setStringValues(stringArray).build();
+        assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+        assertThat(genericDocumentParcel.getPropertyMap()).containsExactly("longArray",
+                longArrayProperty, "stringArray", stringArrayProperty);
+    }
+
+    @Test
+    public void testRecreateFromParcelWithParentTypes() {
+        String[] stringArray = new String[]{"Hello", "world"};
+        long[] longArray = new long[]{1L, 2L};
+        double[] doubleArray = new double[]{1.1, 2.2};
+        boolean[] booleanArray = new boolean[]{true, false};
+        byte[][] bytesArray = new byte[][]{{1, 2}};
+        GenericDocumentParcel inDoc = new GenericDocumentParcel.Builder(
+                "namespace1", "id1", "schema1")
+                .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
+                .setScore(42)
+                .setTtlMillis(43)
+                .setCreationTimestampMillis(44)
+                .putInPropertyMap("propStrings", stringArray)
+                .putInPropertyMap("propLongs", longArray)
+                .putInPropertyMap("propDoubles", doubleArray)
+                .putInPropertyMap("propBytes", bytesArray)
+                .putInPropertyMap("propBooleans", booleanArray)
+                .putInPropertyMap(
+                        "propDoc",
+                        new GenericDocumentParcel[]{
+                                new GenericDocumentParcel.Builder(
+                                        "namespace2", "id2", "schema2")
+                                        .putInPropertyMap("propStrings", new String[]{"Goodbye"})
+                                        .putInPropertyMap("propBytes", new byte[][]{{3, 4}})
+                                        .build()})
+                .build();
+
+        // Serialize the document
+        Parcel inParcel = Parcel.obtain();
+        inParcel.writeParcelable(inDoc, /*flags=*/ 0);
+        byte[] data = inParcel.marshall();
+        inParcel.recycle();
+
+        // Deserialize the document
+        Parcel outParcel = Parcel.obtain();
+        outParcel.unmarshall(data, 0, data.length);
+        outParcel.setDataPosition(0);
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel outDoc = outParcel.readParcelable(getClass().getClassLoader());
+        outParcel.recycle();
+
+        // Compare results
+        assertThat(outDoc.getId()).isEqualTo("id1");
+        assertThat(outDoc.getNamespace()).isEqualTo("namespace1");
+        assertThat(outDoc.getSchemaType()).isEqualTo("schema1");
+        assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
+        assertThat(outDoc.getScore()).isEqualTo(42);
+        assertThat(outDoc.getTtlMillis()).isEqualTo(43);
+        assertThat(outDoc.getCreationTimestampMillis()).isEqualTo(44);
+
+        // Properties
+        Map<String, PropertyParcel> propertyMap = outDoc.getPropertyMap();
+        assertThat(propertyMap.get("propStrings").getStringValues()).isEqualTo(stringArray);
+        assertThat(propertyMap.get("propLongs").getLongValues()).isEqualTo(longArray);
+        assertThat(propertyMap.get("propDoubles").getDoubleValues()).isEqualTo(doubleArray);
+        assertThat(propertyMap.get("propBytes").getBytesValues()).isEqualTo(bytesArray);
+        assertThat(propertyMap.get("propBooleans").getBooleanValues()).isEqualTo(booleanArray);
+
+        // Check inner doc.
+        GenericDocumentParcel[] innerDocs = propertyMap.get("propDoc").getDocumentValues();
+        assertThat(innerDocs).hasLength(1);
+        assertThat(innerDocs[0].getNamespace()).isEqualTo("namespace2");
+        assertThat(innerDocs[0].getId()).isEqualTo("id2");
+        assertThat(innerDocs[0].getSchemaType()).isEqualTo("schema2");
+        assertThat(innerDocs[0].getPropertyMap().get("propStrings").getStringValues()).isEqualTo(
+                new String[]{"Goodbye"});
+        assertThat(innerDocs[0].getPropertyMap().get("propBytes").getBytesValues()).isEqualTo(
+                new byte[][]{{3, 4}});
+
+        // Finally check equals and hashcode
+        assertThat(inDoc).isEqualTo(outDoc);
+        assertThat(inDoc.hashCode()).isEqualTo(outDoc.hashCode());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
new file mode 100644
index 0000000..a5ce50b
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.safeparcel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class PropertyParcelTest {
+    @Test
+    public void testTwoDimensionByteArray_serializationSupported() {
+        int row = 20;
+        int col = 10;
+        byte[][] bytesArray = new byte[row][col];
+        for (int i = 0; i < row; ++i) {
+            for (int j = 0; j < col; ++j) {
+                bytesArray[i][j] = (byte) (i + j);
+            }
+        }
+
+        String propertyName = "propertyName";
+        PropertyParcel expectedPropertyParcel =
+                new PropertyParcel.Builder(propertyName).setBytesValues(bytesArray).build();
+        Parcel data = Parcel.obtain();
+        try {
+            data.writeParcelable(expectedPropertyParcel, /* flags= */ 0);
+            data.setDataPosition(0);
+            @SuppressWarnings("deprecation")
+            PropertyParcel actualPropertyParcel = data.readParcelable(
+                    PropertyParcelTest.class.getClassLoader());
+            assertThat(expectedPropertyParcel).isEqualTo(actualPropertyParcel);
+        } finally {
+            data.recycle();
+        }
+    }
+}
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 59415fb..935c5bf 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.LongSerializer;
 import androidx.appsearch.app.StringSerializer;
 
@@ -228,7 +229,6 @@
          * <p>If not specified, defaults to {@link
          * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed
          * and cannot be queried).
-         * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
          */
         @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType()
                 default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
@@ -540,6 +540,42 @@
     }
 
     /**
+     * Configures an {@link EmbeddingVector} field of a class as a property known to AppSearch.
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target({ElementType.FIELD, ElementType.METHOD})
+    @interface EmbeddingProperty {
+        /**
+         * The name of this property. This string is used to query against this property.
+         *
+         * <p>If not specified, the name of the field in the code will be used instead.
+         */
+        String name() default "";
+
+        /**
+         * Configures how a property should be indexed so that it can be retrieved by queries.
+         *
+         * <p>If not specified, defaults to
+         * {@link AppSearchSchema.EmbeddingPropertyConfig#INDEXING_TYPE_NONE} (the field will not be
+         * indexed and cannot be queried).
+         */
+        @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType()
+                default AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+
+        /**
+         * Configures whether this property must be specified for the document to be valid.
+         *
+         * <p>This attribute does not apply to properties of a repeated type (e.g. a list).
+         *
+         * <p>Please make sure you understand the consequences of required fields on
+         * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before
+         * setting this attribute to {@code true}.
+         */
+        boolean required() default false;
+    }
+
+    /**
      * Marks a static method or a builder class directly as a builder producer. A builder class
      * should contain a "build()" method to construct the AppSearch document object and setter
      * methods to set field values.
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 8263467..08a16d2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -44,12 +44,14 @@
  * @see AppSearchSession#removeAsync
  */
 public final class AppSearchBatchResult<KeyType, ValueType> {
-    @NonNull private final Map<KeyType, ValueType> mSuccesses;
+    @NonNull private final Map<KeyType,
+            @androidx.appsearch.checker.nullness.qual.Nullable ValueType> mSuccesses;
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
 
     AppSearchBatchResult(
-            @NonNull Map<KeyType, ValueType> successes,
+            @NonNull Map<KeyType, @androidx.appsearch.checker.nullness.qual.Nullable ValueType>
+                    successes,
             @NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
             @NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
         mSuccesses = Preconditions.checkNotNull(successes);
@@ -123,7 +125,8 @@
      * @param <ValueType> The type of the result objects for successful results.
      */
     public static final class Builder<KeyType, ValueType> {
-        private ArrayMap<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+        private ArrayMap<KeyType, @androidx.appsearch.checker.nullness.qual.Nullable ValueType>
+                mSuccesses = new ArrayMap<>();
         private ArrayMap<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
         private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
         private boolean mBuilt = false;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
new file mode 100644
index 0000000..aa5326b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.File;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An interface which exposes environment specific methods for AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchEnvironment {
+
+    /** Returns the directory to initialize appsearch based on the environment. */
+    @NonNull
+    File getAppSearchDir(@NonNull Context context, @Nullable UserHandle userHandle);
+
+    /** Returns the correct context for the user based on the environment. */
+    @NonNull
+    Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle);
+
+    /** Returns an ExecutorService based on given parameters. */
+    @NonNull
+    ExecutorService createExecutorService(
+            int corePoolSize,
+            int maxConcurrency,
+            long keepAliveTime,
+            @NonNull TimeUnit unit,
+            @NonNull BlockingQueue<Runnable> workQueue,
+            int priority);
+
+    /** Returns an ExecutorService with a single thread. */
+    @NonNull
+    ExecutorService createSingleThreadExecutor();
+
+    /** Creates and returns an Executor with cached thread pools. */
+    @NonNull
+    ExecutorService createCachedThreadPoolExecutor();
+
+    /**
+     * Returns a cache directory for creating temporary files like in case of migrating documents.
+     */
+    @Nullable
+    File getCacheDir(@NonNull Context context);
+
+    /** Returns if we can log INFO level logs. */
+    boolean isInfoLoggingEnabled();
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
new file mode 100644
index 0000000..9ae8ac1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is a factory class for implementations needed based on the environment.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchEnvironmentFactory {
+    private static volatile AppSearchEnvironment sAppSearchEnvironment;
+
+    /** Returns the singleton instance of {@link AppSearchEnvironment}. */
+    @NonNull
+    public static AppSearchEnvironment getEnvironmentInstance() {
+        AppSearchEnvironment localRef = sAppSearchEnvironment;
+        if (localRef == null) {
+            synchronized (AppSearchEnvironmentFactory.class) {
+                localRef = sAppSearchEnvironment;
+                if (localRef == null) {
+                    sAppSearchEnvironment = localRef =
+                            new JetpackAppSearchEnvironment();
+                }
+            }
+        }
+        return localRef;
+    }
+
+    /** Sets an instance of {@link AppSearchEnvironment}. for testing.*/
+    @VisibleForTesting
+    public static void setEnvironmentInstanceForTest(
+            @NonNull AppSearchEnvironment appSearchEnvironment) {
+        synchronized (AppSearchEnvironmentFactory.class) {
+            sAppSearchEnvironment = appSearchEnvironment;
+        }
+    }
+
+    private AppSearchEnvironmentFactory() {
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
index d8c79a5..86c8102 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -22,6 +22,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.util.LogUtil;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
@@ -54,6 +56,7 @@
             RESULT_SECURITY_ERROR,
             RESULT_DENIED,
             RESULT_RATE_LIMITED,
+            RESULT_TIMED_OUT
     })
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
@@ -101,20 +104,22 @@
     /**
      * The requested operation is denied for the caller. This error is logged and returned for
      * denylist rejections.
-     * <!--@exportToFramework:hide-->
      */
-    // TODO(b/279047435): unhide this the next time we can make API changes
+    @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
     public static final int RESULT_DENIED = 9;
 
     /**
-     * The caller has hit AppSearch's rate limit and the requested operation has been rejected.
-     * <!--@exportToFramework:hide-->
+     * The caller has hit AppSearch's rate limit and the requested operation has been rejected. The
+     * caller is recommended to reschedule tasks with exponential backoff.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    // TODO(b/279047435): unhide this the next time we can make API changes
+    @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
     public static final int RESULT_RATE_LIMITED = 10;
 
-    private final @ResultCode int mResultCode;
+    /** The operation was timed out. */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    public static final int RESULT_TIMED_OUT = 11;
+
+    @ResultCode private final int mResultCode;
     @Nullable private final ValueType mResultValue;
     @Nullable private final String mErrorMessage;
 
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 a4fd79ef..b300d3e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -27,7 +29,15 @@
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.exceptions.IllegalSchemaException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.PropertyConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.AppSearchSchemaCreator;
 import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
@@ -41,6 +51,7 @@
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -52,28 +63,36 @@
  *
  * @see AppSearchSession#setSchemaAsync
  */
-public final class AppSearchSchema {
-    private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String PROPERTIES_FIELD = "properties";
-    private static final String PARENT_TYPES_FIELD = "parentTypes";
-
-    private final Bundle mBundle;
-
-    /** @exportToFramework:hide */
[email protected](creator = "AppSearchSchemaCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class AppSearchSchema extends AbstractSafeParcelable {
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public AppSearchSchema(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
     @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    public static final Parcelable.Creator<AppSearchSchema> CREATOR = new AppSearchSchemaCreator();
+
+    @Field(id = 1, getter = "getSchemaType")
+    private final String mSchemaType;
+
+    @Field(id = 2)
+    final List<PropertyConfigParcel> mPropertyConfigParcels;
+
+    @Field(id = 3, getter = "getParentTypes")
+    private final List<String> mParentTypes;
+
+    @Field(id = 4, getter = "getDescription")
+    private final String mDescription;
+
+    @Constructor
+    AppSearchSchema(
+            @Param(id = 1) @NonNull String schemaType,
+            @Param(id = 2) @NonNull List<PropertyConfigParcel> propertyConfigParcels,
+            @Param(id = 3) @NonNull List<String> parentTypes,
+            @Param(id = 4) @NonNull String description) {
+        mSchemaType = Objects.requireNonNull(schemaType);
+        mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);
+        mParentTypes = Objects.requireNonNull(parentTypes);
+        mDescription = Objects.requireNonNull(description);
     }
 
     @Override
@@ -88,7 +107,7 @@
      * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
      * builder.
      *
-     * @param builder     the builder to append to.
+     * @param builder the builder to append to.
      */
     private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
         Preconditions.checkNotNull(builder);
@@ -96,6 +115,7 @@
         builder.append("{\n");
         builder.increaseIndentLevel();
         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+        builder.append("description: \"").append(getDescription()).append("\",\n");
         builder.append("properties: [\n");
 
         AppSearchSchema.PropertyConfig[] sortedProperties = getProperties()
@@ -121,7 +141,22 @@
     /** Returns the name of this schema type, such as Email. */
     @NonNull
     public String getSchemaType() {
-        return mBundle.getString(SCHEMA_TYPE_FIELD, "");
+        return mSchemaType;
+    }
+
+    /**
+     * Returns a natural language description of this schema type.
+     *
+     * <p>Ex. The description for an Email type could be "A type of electronic message".
+     *
+     * <p>This information is purely to help apps consuming this type to understand its semantic
+     * meaning. This field has no effect in AppSearch - it is just stored with the AppSearchSchema.
+     * If {@link Builder#setDescription} is uncalled, then this method will return an empty string.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    @NonNull
+    public String getDescription() {
+        return mDescription;
     }
 
     /**
@@ -130,35 +165,25 @@
      * <p>This method creates a new list when called.
      */
     @NonNull
-    @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"})
+    @SuppressWarnings({"MixedMutabilityReturnType"})
     public List<PropertyConfig> getProperties() {
-        ArrayList<Bundle> propertyBundles =
-                mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);
-        if (propertyBundles == null || propertyBundles.isEmpty()) {
+        if (mPropertyConfigParcels.isEmpty()) {
             return Collections.emptyList();
         }
-        List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
-        for (int i = 0; i < propertyBundles.size(); i++) {
-            ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
+        List<PropertyConfig> ret = new ArrayList<>(mPropertyConfigParcels.size());
+        for (int i = 0; i < mPropertyConfigParcels.size(); i++) {
+            ret.add(PropertyConfig.fromParcel(mPropertyConfigParcels.get(i)));
         }
         return ret;
     }
 
     /**
      * Returns the list of parent types of this schema for polymorphism.
-     *
-     * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-     * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline
-     *   are possible.
-     * -->
      */
+    @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
     @NonNull
     public List<String> getParentTypes() {
-        List<String> parentTypes = mBundle.getStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD);
-        if (parentTypes == null) {
-            return Collections.emptyList();
-        }
-        return Collections.unmodifiableList(parentTypes);
+        return Collections.unmodifiableList(mParentTypes);
     }
 
     @Override
@@ -173,6 +198,9 @@
         if (!getSchemaType().equals(otherSchema.getSchemaType())) {
             return false;
         }
+        if (!getDescription().equals(otherSchema.getDescription())) {
+            return false;
+        }
         if (!getParentTypes().equals(otherSchema.getParentTypes())) {
             return false;
         }
@@ -181,21 +209,51 @@
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(getSchemaType(), getProperties(), getParentTypes());
+        return ObjectsCompat.hash(
+            getSchemaType(),
+            getProperties(),
+            getParentTypes(),
+            getDescription());
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        AppSearchSchemaCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link AppSearchSchema objects}. */
     public static final class Builder {
         private final String mSchemaType;
-        private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
+        private String mDescription = "";
+        private ArrayList<PropertyConfigParcel> mPropertyConfigParcels = new ArrayList<>();
         private LinkedHashSet<String> mParentTypes = new LinkedHashSet<>();
         private final Set<String> mPropertyNames = new ArraySet<>();
         private boolean mBuilt = false;
 
         /** Creates a new {@link AppSearchSchema.Builder}. */
         public Builder(@NonNull String schemaType) {
-            Preconditions.checkNotNull(schemaType);
-            mSchemaType = schemaType;
+            mSchemaType = Preconditions.checkNotNull(schemaType);
+        }
+
+        /**
+         * Sets a natural language description of this schema type.
+         *
+         * <p> For more details about the description field, see {@link
+         * AppSearchSchema#getDescription}.
+         */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SCHEMA_SET_DESCRIPTION)
+        @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+        @CanIgnoreReturnValue
+        @NonNull
+        public AppSearchSchema.Builder setDescription(@NonNull String description) {
+            Objects.requireNonNull(description);
+            resetIfBuilt();
+            mDescription = description;
+            return this;
         }
 
         /** Adds a property to the given type. */
@@ -208,7 +266,7 @@
             if (!mPropertyNames.add(name)) {
                 throw new IllegalSchemaException("Property defined more than once: " + name);
             }
-            mPropertyBundles.add(propertyConfig.mBundle);
+            mPropertyConfigParcels.add(propertyConfig.mPropertyConfigParcel);
             return this;
         }
 
@@ -273,11 +331,9 @@
          */
         @CanIgnoreReturnValue
         @NonNull
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SCHEMA_ADD_PARENT_TYPE)
-        // @exportToFramework:endStrip()
         public AppSearchSchema.Builder addParentType(@NonNull String parentSchemaType) {
             Preconditions.checkNotNull(parentSchemaType);
             resetIfBuilt();
@@ -288,18 +344,16 @@
         /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
         @NonNull
         public AppSearchSchema build() {
-            Bundle bundle = new Bundle();
-            bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
-            bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
-            bundle.putStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD,
-                    new ArrayList<>(mParentTypes));
             mBuilt = true;
-            return new AppSearchSchema(bundle);
+            return new AppSearchSchema(mSchemaType,
+                    mPropertyConfigParcels,
+                    new ArrayList<>(mParentTypes),
+                    mDescription);
         }
 
         private void resetIfBuilt() {
             if (mBuilt) {
-                mPropertyBundles = new ArrayList<>(mPropertyBundles);
+                mPropertyConfigParcels = new ArrayList<>(mPropertyConfigParcels);
                 mParentTypes = new LinkedHashSet<>(mParentTypes);
                 mBuilt = false;
             }
@@ -313,10 +367,6 @@
      * a property.
      */
     public abstract static class PropertyConfig {
-        static final String NAME_FIELD = "name";
-        static final String DATA_TYPE_FIELD = "dataType";
-        static final String CARDINALITY_FIELD = "cardinality";
-
         /**
          * Physical data-types of the contents of the property.
          *
@@ -333,28 +383,47 @@
                 DATA_TYPE_BOOLEAN,
                 DATA_TYPE_BYTES,
                 DATA_TYPE_DOCUMENT,
+                DATA_TYPE_EMBEDDING,
         })
         @Retention(RetentionPolicy.SOURCE)
-        public @interface DataType {}
+        public @interface DataType {
+        }
 
-        /** @exportToFramework:hide */
+        /**
+         * Constant value for String data type.
+         *
+         * @exportToFramework:hide
+         */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_STRING = 1;
 
-        /** @exportToFramework:hide */
+        /**
+         * Constant value for Long data type.
+         *
+         * @exportToFramework:hide
+         */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_LONG = 2;
 
-        /** @exportToFramework:hide */
+        /**
+         * Constant value for Double data type.
+         *
+         * @exportToFramework:hide
+         */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_DOUBLE = 3;
 
-        /** @exportToFramework:hide */
+        /**
+         * Constant value for Boolean data type.
+         *
+         * @exportToFramework:hide
+         */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_BOOLEAN = 4;
 
         /**
          * Unstructured BLOB.
+         *
          * @exportToFramework:hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -364,12 +433,21 @@
          * Indicates that the property is itself a {@link GenericDocument}, making it part of a
          * hierarchical schema. Any property using this DataType MUST have a valid
          * {@link PropertyConfig#getSchemaType}.
+         *
          * @exportToFramework:hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DATA_TYPE_DOCUMENT = 6;
 
         /**
+         * Indicates that the property is an {@link EmbeddingVector}.
+         *
+         * @exportToFramework:hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public static final int DATA_TYPE_EMBEDDING = 7;
+
+        /**
          * The cardinality of the property (whether it is required, optional or repeated).
          *
          * <p>NOTE: The integer values of these constants must match the proto enum constants in
@@ -384,7 +462,8 @@
                 CARDINALITY_REQUIRED,
         })
         @Retention(RetentionPolicy.SOURCE)
-        public @interface Cardinality {}
+        public @interface Cardinality {
+        }
 
         /** Any number of items (including zero) [0...*]. */
         public static final int CARDINALITY_REPEATED = 1;
@@ -395,13 +474,10 @@
         /** Exactly one value [1]. */
         public static final int CARDINALITY_REQUIRED = 3;
 
-        final Bundle mBundle;
+        final PropertyConfigParcel mPropertyConfigParcel;
 
-        @Nullable
-        private Integer mHashCode;
-
-        PropertyConfig(@NonNull Bundle bundle) {
-            mBundle = Preconditions.checkNotNull(bundle);
+        PropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            mPropertyConfigParcel = Preconditions.checkNotNull(propertyConfigParcel);
         }
 
         @Override
@@ -416,7 +492,7 @@
          * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
          * given string builder.
          *
-         * @param builder        the builder to append to.
+         * @param builder the builder to append to.
          */
         void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
             Preconditions.checkNotNull(builder);
@@ -424,6 +500,7 @@
             builder.append("{\n");
             builder.increaseIndentLevel();
             builder.append("name: \"").append(getName()).append("\",\n");
+            builder.append("description: \"").append(getDescription()).append("\",\n");
 
             if (this instanceof AppSearchSchema.StringPropertyConfig) {
                 ((StringPropertyConfig) this)
@@ -469,6 +546,9 @@
                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
                     builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
                     break;
+                case PropertyConfig.DATA_TYPE_EMBEDDING:
+                    builder.append("dataType: DATA_TYPE_EMBEDDING,\n");
+                    break;
                 default:
                     builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
             }
@@ -479,7 +559,24 @@
         /** Returns the name of this property. */
         @NonNull
         public String getName() {
-            return mBundle.getString(NAME_FIELD, "");
+            return mPropertyConfigParcel.getName();
+        }
+
+        /**
+         * Returns a natural language description of this property.
+         *
+         * <p>Ex. The description for the "homeAddress" property of a "Person" type could be "the
+         * address at which this person lives".
+         *
+         * <p>This information is purely to help apps consuming this type the semantic meaning of
+         * its properties. This field has no effect in AppSearch - it is just stored with the
+         * AppSearchSchema. If the description is not set, then this method will return an empty
+         * string.
+         */
+        @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+        @NonNull
+        public String getDescription() {
+            return mPropertyConfigParcel.getDescription();
         }
 
         /**
@@ -490,7 +587,7 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @DataType
         public int getDataType() {
-            return mBundle.getInt(DATA_TYPE_FIELD, -1);
+            return mPropertyConfigParcel.getDataType();
         }
 
         /**
@@ -498,7 +595,7 @@
          */
         @Cardinality
         public int getCardinality() {
-            return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+            return mPropertyConfigParcel.getCardinality();
         }
 
         @Override
@@ -510,15 +607,12 @@
                 return false;
             }
             PropertyConfig otherProperty = (PropertyConfig) other;
-            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+            return ObjectsCompat.equals(mPropertyConfigParcel, otherProperty.mPropertyConfigParcel);
         }
 
         @Override
         public int hashCode() {
-            if (mHashCode == null) {
-                mHashCode = BundleUtil.deepHashCode(mBundle);
-            }
-            return mHashCode;
+            return mPropertyConfigParcel.hashCode();
         }
 
         /**
@@ -528,41 +622,40 @@
          * <p>The bundle is not cloned.
          *
          * @throws IllegalArgumentException if the bundle does no contain a recognized
-         * value in its {@code DATA_TYPE_FIELD}.
+         *                                  value in its {@code DATA_TYPE_FIELD}.
          * @exportToFramework:hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @NonNull
-        public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
-            switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
+        public static PropertyConfig fromParcel(
+                @NonNull PropertyConfigParcel propertyConfigParcel) {
+            Preconditions.checkNotNull(propertyConfigParcel);
+            switch (propertyConfigParcel.getDataType()) {
                 case PropertyConfig.DATA_TYPE_STRING:
-                    return new StringPropertyConfig(propertyBundle);
+                    return new StringPropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_LONG:
-                    return new LongPropertyConfig(propertyBundle);
+                    return new LongPropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_DOUBLE:
-                    return new DoublePropertyConfig(propertyBundle);
+                    return new DoublePropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_BOOLEAN:
-                    return new BooleanPropertyConfig(propertyBundle);
+                    return new BooleanPropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_BYTES:
-                    return new BytesPropertyConfig(propertyBundle);
+                    return new BytesPropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_DOCUMENT:
-                    return new DocumentPropertyConfig(propertyBundle);
+                    return new DocumentPropertyConfig(propertyConfigParcel);
+                case PropertyConfig.DATA_TYPE_EMBEDDING:
+                    return new EmbeddingPropertyConfig(propertyConfigParcel);
                 default:
                     throw new IllegalArgumentException(
                             "Unsupported property bundle of type "
-                                    + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
-                                    + "; contents: " + propertyBundle);
+                                    + propertyConfigParcel.getDataType()
+                                    + "; contents: " + propertyConfigParcel);
             }
         }
     }
 
     /** Configuration for a property of type String in a Document. */
     public static final class StringPropertyConfig extends PropertyConfig {
-        private static final String INDEXING_TYPE_FIELD = "indexingType";
-        private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
-        private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType";
-        private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation";
-
         /**
          * Encapsulates the configurations on how AppSearch should query/index these terms.
          * @exportToFramework:hide
@@ -574,7 +667,8 @@
                 INDEXING_TYPE_PREFIXES,
         })
         @Retention(RetentionPolicy.SOURCE)
-        public @interface IndexingType {}
+        public @interface IndexingType {
+        }
 
         /** Content in this property will not be tokenized or indexed. */
         public static final int INDEXING_TYPE_NONE = 0;
@@ -611,7 +705,8 @@
                 TOKENIZER_TYPE_RFC822
         })
         @Retention(RetentionPolicy.SOURCE)
-        public @interface TokenizerType {}
+        public @interface TokenizerType {
+        }
 
         /**
          * This value indicates that no tokens should be extracted from this property.
@@ -646,11 +741,9 @@
          * <p>It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is
          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
          */
-// @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.VERBATIM_SEARCH)
-// @exportToFramework:endStrip()
         public static final int TOKENIZER_TYPE_VERBATIM = 2;
 
         /**
@@ -663,11 +756,9 @@
          * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
          */
-// @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.TOKENIZER_TYPE_RFC822)
-// @exportToFramework:endStrip()
         public static final int TOKENIZER_TYPE_RFC822 = 3;
 
         /**
@@ -684,7 +775,8 @@
         })
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @Retention(RetentionPolicy.SOURCE)
-        public @interface JoinableValueType {}
+        public @interface JoinableValueType {
+        }
 
         /** Content in this property is not joinable. */
         public static final int JOINABLE_VALUE_TYPE_NONE = 0;
@@ -700,27 +792,37 @@
          *     {@link PropertyConfig#CARDINALITY_REQUIRED}.
          * </ul>
          */
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
-        // @exportToFramework:endStrip()
         public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1;
 
-        StringPropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        StringPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Returns how the property is indexed. */
-        @IndexingType
+        @StringPropertyConfig.IndexingType
         public int getIndexingType() {
-            return mBundle.getInt(INDEXING_TYPE_FIELD);
+            StringIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getStringIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+
+            return indexingConfigParcel.getIndexingType();
         }
 
         /** Returns how this property is tokenized (split into words). */
         @TokenizerType
         public int getTokenizerType() {
-            return mBundle.getInt(TOKENIZER_TYPE_FIELD);
+            StringIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getStringIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return TOKENIZER_TYPE_NONE;
+            }
+
+            return indexingConfigParcel.getTokenizerType();
         }
 
         /**
@@ -728,27 +830,27 @@
          */
         @JoinableValueType
         public int getJoinableValueType() {
-            return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE);
-        }
+            JoinableConfigParcel joinableConfigParcel = mPropertyConfigParcel
+                    .getJoinableConfigParcel();
+            if (joinableConfigParcel == null) {
+                return JOINABLE_VALUE_TYPE_NONE;
+            }
 
-        /**
-         * Returns whether or not documents in this schema should be deleted when the document
-         * referenced by this field is deleted.
-         *
-         * @see JoinSpec
-         * @<!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()hide-->
-         */
-        public boolean getDeletionPropagation() {
-            return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false);
+            return joinableConfigParcel.getJoinableValueType();
         }
 
         /** Builder for {@link StringPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
-            @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
-            @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;
-            @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
+            @StringPropertyConfig.IndexingType
+            private int mIndexingType = INDEXING_TYPE_NONE;
+            @TokenizerType
+            private int mTokenizerType = TOKENIZER_TYPE_NONE;
+            @JoinableValueType
+            private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
             private boolean mDeletionPropagation = false;
 
             /** Creates a new {@link StringPropertyConfig.Builder}. */
@@ -757,6 +859,24 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public StringPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -781,7 +901,8 @@
              */
             @CanIgnoreReturnValue
             @NonNull
-            public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+            public StringPropertyConfig.Builder setIndexingType(
+                    @StringPropertyConfig.IndexingType int indexingType) {
                 Preconditions.checkArgumentInRange(
                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
                 mIndexingType = indexingType;
@@ -830,25 +951,6 @@
             }
 
             /**
-             * Configures whether or not documents in this schema will be removed when the document
-             * referred to by this property is deleted.
-             *
-             * <p> Requires that a joinable value type is set.
-             * @<!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()hide-->
-             */
-            @SuppressWarnings("MissingGetterMatchingBuilder")  // getDeletionPropagation
-            @NonNull
-            // @exportToFramework:startStrip()
-            @RequiresFeature(
-                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
-                    name = Features.SCHEMA_SET_DELETION_PROPAGATION)
-            // @exportToFramework:endStrip()
-            public Builder setDeletionPropagation(boolean deletionPropagation) {
-                mDeletionPropagation = deletionPropagation;
-                return this;
-            }
-
-            /**
              * Constructs a new {@link StringPropertyConfig} from the contents of this builder.
              */
             @NonNull
@@ -868,15 +970,17 @@
                     Preconditions.checkState(!mDeletionPropagation, "Cannot set deletion "
                             + "propagation without setting a joinable value type");
                 }
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
-                bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
-                bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType);
-                bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation);
-                return new StringPropertyConfig(bundle);
+                PropertyConfigParcel.StringIndexingConfigParcel stringConfigParcel =
+                        new StringIndexingConfigParcel(mIndexingType, mTokenizerType);
+                JoinableConfigParcel joinableConfigParcel =
+                        new JoinableConfigParcel(mJoinableValueType, mDeletionPropagation);
+                return new StringPropertyConfig(
+                        PropertyConfigParcel.createForString(
+                                mPropertyName,
+                                mDescription,
+                                mCardinality,
+                                stringConfigParcel,
+                                joinableConfigParcel));
             }
         }
 
@@ -886,7 +990,7 @@
          *
          * <p>This appends fields specific to a {@link StringPropertyConfig} instance.
          *
-         * @param builder        the builder to append to.
+         * @param builder the builder to append to.
          */
         void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
             switch (getIndexingType()) {
@@ -935,11 +1039,10 @@
 
     /** Configuration for a property containing a 64-bit integer. */
     public static final class LongPropertyConfig extends PropertyConfig {
-        private static final String INDEXING_TYPE_FIELD = "indexingType";
-
         /**
          * Encapsulates the configurations on how AppSearch should query/index these 64-bit
          * integers.
+         *
          * @exportToFramework:hide
          */
         @IntDef(value = {
@@ -948,7 +1051,8 @@
         })
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @Retention(RetentionPolicy.SOURCE)
-        public @interface IndexingType {}
+        public @interface IndexingType {
+        }
 
         /** Content in this property will not be indexed. */
         public static final int INDEXING_TYPE_NONE = 0;
@@ -959,28 +1063,34 @@
          *
          * <p>For example, a property with 1024 should match numeric search range query [0, 2000].
          */
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.NUMERIC_SEARCH)
-        // @exportToFramework:endStrip()
         public static final int INDEXING_TYPE_RANGE = 1;
 
-        LongPropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        LongPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Returns how the property is indexed. */
-        @IndexingType
+        @LongPropertyConfig.IndexingType
         public int getIndexingType() {
-            return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
+            PropertyConfigParcel.IntegerIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getIntegerIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+            return indexingConfigParcel.getIndexingType();
         }
 
         /** Builder for {@link LongPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
-            @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
+            @LongPropertyConfig.IndexingType
+            private int mIndexingType = INDEXING_TYPE_NONE;
 
             /** Creates a new {@link LongPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
@@ -988,6 +1098,24 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public LongPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -1012,7 +1140,8 @@
              */
             @CanIgnoreReturnValue
             @NonNull
-            public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+            public LongPropertyConfig.Builder setIndexingType(
+                    @LongPropertyConfig.IndexingType int indexingType) {
                 Preconditions.checkArgumentInRange(
                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType");
                 mIndexingType = indexingType;
@@ -1022,12 +1151,9 @@
             /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
             @NonNull
             public LongPropertyConfig build() {
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
-                return new LongPropertyConfig(bundle);
+                return new LongPropertyConfig(
+                        PropertyConfigParcel.createForLong(
+                                mPropertyName, mDescription, mCardinality, mIndexingType));
             }
         }
 
@@ -1037,7 +1163,7 @@
          *
          * <p>This appends fields specific to a {@link LongPropertyConfig} instance.
          *
-         * @param builder        the builder to append to.
+         * @param builder the builder to append to.
          */
         void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
             switch (getIndexingType()) {
@@ -1055,14 +1181,16 @@
 
     /** Configuration for a property containing a double-precision decimal number. */
     public static final class DoublePropertyConfig extends PropertyConfig {
-        DoublePropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        DoublePropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Builder for {@link DoublePropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link DoublePropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
@@ -1070,6 +1198,24 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public DoublePropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -1088,25 +1234,25 @@
             /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
             @NonNull
             public DoublePropertyConfig build() {
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                return new DoublePropertyConfig(bundle);
+                return new DoublePropertyConfig(
+                        PropertyConfigParcel.createForDouble(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
 
     /** Configuration for a property containing a boolean. */
     public static final class BooleanPropertyConfig extends PropertyConfig {
-        BooleanPropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        BooleanPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Builder for {@link BooleanPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link BooleanPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
@@ -1114,6 +1260,24 @@
             }
 
             /**
+              Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public BooleanPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -1132,25 +1296,25 @@
             /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
             @NonNull
             public BooleanPropertyConfig build() {
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                return new BooleanPropertyConfig(bundle);
+                return new BooleanPropertyConfig(
+                        PropertyConfigParcel.createForBoolean(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
 
     /** Configuration for a property containing a byte array. */
     public static final class BytesPropertyConfig extends PropertyConfig {
-        BytesPropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        BytesPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Builder for {@link BytesPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link BytesPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
@@ -1158,6 +1322,24 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public BytesPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -1178,30 +1360,23 @@
              */
             @NonNull
             public BytesPropertyConfig build() {
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                return new BytesPropertyConfig(bundle);
+                return new BytesPropertyConfig(
+                        PropertyConfigParcel.createForBytes(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
 
     /** Configuration for a property containing another Document. */
     public static final class DocumentPropertyConfig extends PropertyConfig {
-        private static final String SCHEMA_TYPE_FIELD = "schemaType";
-        private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
-        private static final String INDEXABLE_NESTED_PROPERTIES_LIST_FIELD =
-                "indexableNestedPropertiesList";
-
-        DocumentPropertyConfig(@NonNull Bundle bundle) {
-            super(bundle);
+        DocumentPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
         }
 
         /** Returns the logical schema-type of the contents of this document property. */
         @NonNull
         public String getSchemaType() {
-            return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+            return Preconditions.checkNotNull(mPropertyConfigParcel.getSchemaType());
         }
 
         /**
@@ -1215,24 +1390,33 @@
          * indexing a subset of properties from the nested document.
          */
         public boolean shouldIndexNestedProperties() {
-            return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
+            DocumentIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return false;
+            }
+
+            return indexingConfigParcel.shouldIndexNestedProperties();
         }
 
         /**
          * Returns the list of indexable nested properties for the nested document.
-         *
-         * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-         * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
-         *   Mainline are possible.
-         * -->
          */
+        @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
         @NonNull
         public List<String> getIndexableNestedProperties() {
+            DocumentIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return Collections.emptyList();
+            }
+
             List<String> indexableNestedPropertiesList =
-                    mBundle.getStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD);
+                    indexingConfigParcel.getIndexableNestedPropertiesList();
             if (indexableNestedPropertiesList == null) {
                 return Collections.emptyList();
             }
+
             return Collections.unmodifiableList(indexableNestedPropertiesList);
         }
 
@@ -1240,7 +1424,9 @@
         public static final class Builder {
             private final String mPropertyName;
             private final String mSchemaType;
-            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
             private boolean mShouldIndexNestedProperties = false;
             private final Set<String> mIndexableNestedPropertiesList = new ArraySet<>();
 
@@ -1250,9 +1436,9 @@
              * @param propertyName The logical name of the property in the schema, which will be
              *                     used as the key for this property in
              *                     {@link GenericDocument.Builder#setPropertyDocument}.
-             * @param schemaType The type of documents which will be stored in this property.
-             *                   Documents of different types cannot be mixed into a single
-             *                   property.
+             * @param schemaType   The type of documents which will be stored in this property.
+             *                     Documents of different types cannot be mixed into a single
+             *                     property.
              */
             public Builder(@NonNull String propertyName, @NonNull String schemaType) {
                 mPropertyName = Preconditions.checkNotNull(propertyName);
@@ -1260,6 +1446,24 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public DocumentPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is
@@ -1297,19 +1501,13 @@
              * Adds one or more properties for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             *
-             * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-             * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
-             *   Mainline are possible.
-             * -->
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @NonNull
-            // @exportToFramework:startStrip()
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
-            // @exportToFramework:endStrip()
             public DocumentPropertyConfig.Builder addIndexableNestedProperties(
                     @NonNull String... indexableNestedProperties) {
                 Preconditions.checkNotNull(indexableNestedProperties);
@@ -1320,20 +1518,14 @@
              * Adds one or more property paths for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             *
-             * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-             * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
-             *   Mainline are possible.
-             * -->
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @SuppressLint("MissingGetterMatchingBuilder")
             @NonNull
-            // @exportToFramework:startStrip()
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
-            // @exportToFramework:endStrip()
             public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(
                     @NonNull PropertyPath... indexableNestedPropertyPaths) {
                 Preconditions.checkNotNull(indexableNestedPropertyPaths);
@@ -1371,11 +1563,9 @@
              */
             @CanIgnoreReturnValue
             @NonNull
-            // @exportToFramework:startStrip()
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
-            // @exportToFramework:endStrip()
             public DocumentPropertyConfig.Builder addIndexableNestedProperties(
                     @NonNull Collection<String> indexableNestedProperties) {
                 Preconditions.checkNotNull(indexableNestedProperties);
@@ -1387,20 +1577,14 @@
              * Adds one or more property paths for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             *
-             * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-             * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
-             *   Mainline are possible.
-             * -->
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @SuppressLint("MissingGetterMatchingBuilder")
             @NonNull
-            // @exportToFramework:startStrip()
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES)
-            // @exportToFramework:endStrip()
             public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(
                     @NonNull Collection<PropertyPath> indexableNestedPropertyPaths) {
                 Preconditions.checkNotNull(indexableNestedPropertyPaths);
@@ -1426,15 +1610,14 @@
                                     + "to be false when one or more indexableNestedProperties are "
                                     + "provided.");
                 }
-                Bundle bundle = new Bundle();
-                bundle.putString(NAME_FIELD, mPropertyName);
-                bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
-                bundle.putInt(CARDINALITY_FIELD, mCardinality);
-                bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
-                bundle.putStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD,
-                        new ArrayList<>(mIndexableNestedPropertiesList));
-                bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);
-                return new DocumentPropertyConfig(bundle);
+                return new DocumentPropertyConfig(
+                        PropertyConfigParcel.createForDocument(
+                                mPropertyName,
+                                mDescription,
+                                mCardinality,
+                                mSchemaType,
+                                new DocumentIndexingConfigParcel(mShouldIndexNestedProperties,
+                                        new ArrayList<>(mIndexableNestedPropertiesList))));
             }
         }
 
@@ -1444,7 +1627,7 @@
          *
          * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
          *
-         * @param builder        the builder to append to.
+         * @param builder the builder to append to.
          */
         void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
             builder
@@ -1459,4 +1642,133 @@
             builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
         }
     }
+
+    /**
+     * Configuration for a property of type {@link EmbeddingVector} in a Document.
+     */
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final class EmbeddingPropertyConfig extends PropertyConfig {
+        /**
+         * Encapsulates the configurations on how AppSearch should query/index these embedding
+         * vectors.
+         *
+         * @exportToFramework:hide
+         */
+        @IntDef(value = {
+                INDEXING_TYPE_NONE,
+                INDEXING_TYPE_SIMILARITY
+        })
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface IndexingType {
+        }
+
+        /** Content in this property will not be indexed. */
+        public static final int INDEXING_TYPE_NONE = 0;
+
+        /**
+         * Embedding vectors in this property will be indexed.
+         *
+         * <p>The index offers 100% accuracy, but has linear time complexity based on the number
+         * of embedding vectors within the index.
+         */
+        public static final int INDEXING_TYPE_SIMILARITY = 1;
+
+        EmbeddingPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
+        }
+
+        /** Returns how the property is indexed. */
+        @EmbeddingPropertyConfig.IndexingType
+        public int getIndexingType() {
+            PropertyConfigParcel.EmbeddingIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getEmbeddingIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+            return indexingConfigParcel.getIndexingType();
+        }
+
+        /** Builder for {@link EmbeddingPropertyConfig}. */
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public static final class Builder {
+            private final String mPropertyName;
+            private String mDescription = "";
+            @Cardinality
+            private int mCardinality = CARDINALITY_OPTIONAL;
+            @EmbeddingPropertyConfig.IndexingType
+            private int mIndexingType = INDEXING_TYPE_NONE;
+
+            /** Creates a new {@link EmbeddingPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Preconditions.checkNotNull(propertyName);
+            }
+
+            /**
+             * Sets a natural language description of this property.
+             *
+             * <p> For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @RequiresFeature(
+                    enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                    name = Features.SCHEMA_SET_DESCRIPTION)
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
+             * Sets the cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @CanIgnoreReturnValue
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /**
+             * Configures how a property should be indexed so that it can be retrieved by queries.
+             *
+             * <p>If this method is not called, the default indexing type is
+             * {@link EmbeddingPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed
+             * and cannot be matched by queries.
+             */
+            @CanIgnoreReturnValue
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setIndexingType(
+                    @EmbeddingPropertyConfig.IndexingType int indexingType) {
+                Preconditions.checkArgumentInRange(
+                        indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_SIMILARITY,
+                        "indexingType");
+                mIndexingType = indexingType;
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link EmbeddingPropertyConfig} from the contents of this
+             * builder.
+             */
+            @NonNull
+            public EmbeddingPropertyConfig build() {
+                return new EmbeddingPropertyConfig(
+                        PropertyConfigParcel.createForEmbedding(
+                                mPropertyName, mDescription, mCardinality, mIndexingType));
+            }
+        }
+    }
 }
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 05d1815..58821b7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -176,7 +176,7 @@
      * <p>The newly added custom functions covered by this feature are:
      * <ul>
      *     <li>createList(String...)</li>
-     *     <li>search(String, List<String>)</li>
+     *     <li>search(String, {@code List<String>})</li>
      *     <li>propertyDefined(String)</li>
      * </ul>
      *
@@ -187,13 +187,14 @@
      * query language and an optional list of strings that specify the properties to be
      * restricted to. This exists as a convenience for multiple property restricts. So,
      * for example, the query `(subject:foo OR body:foo) (subject:bar OR body:bar)`
-     * could be rewritten as `search("foo bar", createList("subject", "bar"))`.
+     * could be rewritten as `search("foo bar", createList("subject", "body"))`.
      *
      * <p>propertyDefined takes a string specifying the property of interest and matches all
      * documents of any type that defines the specified property
      * (ex. `propertyDefined("sender.name")`). Note that propertyDefined will match so long as
-     * the document's type defines the specified property. It does NOT require that the document
-     * actually hold any values for this property.
+     * the document's type defines the specified property. Unlike the "hasProperty" function
+     * below, this function does NOT require that the document actually hold any values for this
+     * property.
      *
      * <p>{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
      * query language, the values of properties that have
@@ -209,6 +210,68 @@
      *
      * <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
      *
+     * <p>{@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}: This feature covers the
+     * "hasProperty" function in query expressions, which takes a string specifying the property
+     * of interest and matches all documents that hold values for this property. Not to be
+     * confused with the "propertyDefined" function, which checks whether a document's schema
+     * has defined the property, instead of whether a document itself has this property.
+     *
+     * <p>Ex. `foo hasProperty("sender.name")` will return all documents that have the term "foo"
+     * AND have values in the property "sender.name". Consider two documents, documentA and
+     * documentB, of the same schema with an optional property "sender.name". If documentA sets
+     * "foo" in this property but documentB does not, then `hasProperty("sender.name")` will only
+     * match documentA. However, `propertyDefined("sender.name")` will match both documentA and
+     * documentB, regardless of whether a value is actually set.
+     *
+     * <p>{@link Features#SCHEMA_EMBEDDING_PROPERTY_CONFIG}: This feature covers the
+     * "semanticSearch" and "getSearchSpecEmbedding" functions in query expressions, which are
+     * used for semantic search.
+     *
+     * <p>Usage: semanticSearch(getSearchSpecEmbedding({embedding_index}), {low}, {high}, {metric})
+     * <ul>
+     *     <li>semanticSearch matches all documents that have at least one embedding vector with
+     *     a matching model signature (see {@link EmbeddingVector#getModelSignature()}) and a
+     *     similarity score within the range specified based on the provided metric.</li>
+     *     <li>getSearchSpecEmbedding({embedding_index}) retrieves the embedding search passed in
+     *     {@link SearchSpec.Builder#addSearchEmbeddings} based on the index specified, which
+     *     starts from 0.</li>
+     *     <li>"low" and "high" are floating point numbers that specify the similarity score
+     *     range. If omitted, they default to negative and positive infinity, respectively.</li>
+     *     <li>"metric" is a string value that specifies how embedding similarities should be
+     *     calculated. If omitted, it defaults to the metric specified in
+     *     {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. Possible
+     *     values:</li>
+     *     <ul>
+     *         <li>"COSINE"</li>
+     *         <li>"DOT_PRODUCT"</li>
+     *         <li>"EUCLIDEAN"</li>
+     *     </ul>
+     * </ul>
+     *
+     * <p>Examples:
+     * <ul>
+     *     <li>Basic: semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")</li>
+     *     <li>With a property restriction:
+     *     property1:semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)</li>
+     *     <li>Hybrid: foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)</li>
+     *     <li>Complex: (foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)) AND bar</li>
+     * </ul>
+     *
+     * <p>{@link Features#LIST_FILTER_TOKENIZE_FUNCTION}: This feature covers the
+     * "tokenize" function in query expressions, which takes a string and treats the entire string
+     * as plain text. This string is then segmented, normalized and stripped of punctuation-only
+     * segments. The remaining tokens are then AND'd together. This function is useful for callers
+     * who wish to provide user input, but want to ensure that that user input does not invoke any
+     * query operators.
+     *
+     * <p>Ex. `foo OR tokenize("bar OR baz.")`. The string "bar OR baz." will be segmented into
+     * "bar", "OR", "baz", ".". Punctuation is removed and the segments are normalized to "bar",
+     * "or", "baz". This query will be equivalent to `foo OR (bar AND or AND baz)`.
+     *
+     * <p>Ex. `tokenize("\"bar\" OR \\baz")`. Quotation marks and escape characters must be escaped.
+     * This query will be segmented into "\"", "bar", "\"", "OR", "\", "baz". Once stripped of
+     * punctuation and normalized, this will be equivalent to the query `bar AND or AND baz`.
+     *
      * <p>The availability of each of these features can be checked by calling
      * {@link Features#isFeatureSupported} with the desired feature.
      *
@@ -223,6 +286,8 @@
      *                        match type, etc.
      * @return a {@link SearchResults} object for retrieved matched documents.
      */
+    // TODO(b/326656531): Refine the javadoc to provide guidance on the best practice of
+    //  embedding searches and how to select an appropriate metric.
     @NonNull
     SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
 
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
new file mode 100644
index 0000000..1addca7
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingVectorCreator;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Embeddings are vector representations of data, such as text, images, and audio, which can be
+ * generated by machine learning models and used for semantic search. This class represents an
+ * embedding vector, which wraps a float array for the values of the embedding vector and a model
+ * signature that can be any string to distinguish between embedding vectors generated by
+ * different models.
+ *
+ * <p>For more details on how embedding search works, check {@link AppSearchSession#search} and
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+ *
+ * @see SearchSpec.Builder#addSearchEmbeddings
+ * @see GenericDocument.Builder#setPropertyEmbedding
+ */
+@RequiresFeature(
+        enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+        name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+@FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
[email protected](creator = "EmbeddingVectorCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class EmbeddingVector extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static final Parcelable.Creator<EmbeddingVector> CREATOR =
+            new EmbeddingVectorCreator();
+    @NonNull
+    @Field(id = 1, getter = "getValues")
+    private final float[] mValues;
+    @NonNull
+    @Field(id = 2, getter = "getModelSignature")
+    private final String mModelSignature;
+    @Nullable
+    private Integer mHashCode;
+
+    /**
+     * Creates a new {@link EmbeddingVector}.
+     *
+     * @throws IllegalArgumentException if {@code values} is empty.
+     */
+    @Constructor
+    public EmbeddingVector(
+            @Param(id = 1) @NonNull float[] values,
+            @Param(id = 2) @NonNull String modelSignature) {
+        mValues = Preconditions.checkNotNull(values);
+        if (mValues.length == 0) {
+            throw new IllegalArgumentException("Embedding values cannot be empty.");
+        }
+        mModelSignature = Preconditions.checkNotNull(modelSignature);
+    }
+
+    /**
+     * Returns the values of this embedding vector.
+     */
+    @NonNull
+    public float[] getValues() {
+        return mValues;
+    }
+
+    /**
+     * Returns the model signature of this embedding vector, which is an arbitrary string to
+     * distinguish between embedding vectors generated by different models.
+     */
+    @NonNull
+    public String getModelSignature() {
+        return mModelSignature;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null) return false;
+        if (!(o instanceof EmbeddingVector)) return false;
+        EmbeddingVector that = (EmbeddingVector) o;
+        return Arrays.equals(mValues, that.mValues)
+                && mModelSignature.equals(that.mModelSignature);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = Objects.hash(Arrays.hashCode(mValues), mModelSignature);
+        }
+        return mHashCode;
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        EmbeddingVectorCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
new file mode 100644
index 0000000..eaaec59
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Provides a connection to all enterprise (work profile) AppSearch databases the querying
+ * application has been granted access to.
+ *
+ * <p>This session can be created from any user profile but will only properly return results when
+ * created from the main profile. If the user is not the main profile or an associated work profile
+ * does not exist, queries will still successfully complete but with empty results.
+ *
+ * <p>Schemas must be explicitly tagged enterprise and may require additional permissions to be
+ * visible from an enterprise session. Retrieved documents may also have certain fields restricted
+ * or modified unlike if they were retrieved directly from {@link GlobalSearchSession} on the work
+ * profile.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see GlobalSearchSession
+ */
+@RequiresFeature(
+        enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+        name = Features.ENTERPRISE_GLOBAL_SEARCH_SESSION)
+public interface EnterpriseGlobalSearchSession {
+    /**
+     * 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 EnterpriseGlobalSearchSession} 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
+    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull GetByDocumentIdRequest request);
+
+    /**
+     * Retrieves documents from all enterprise (work profile) AppSearch databases that the querying
+     * application has access to.
+     *
+     * <p>Applications can be granted access to documents by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.
+     *
+     * <p>Document access can also be granted to system UIs by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem}
+     * when building a schema.
+     *
+     * <p>See {@link AppSearchSession#search} for a detailed explanation on
+     * forming a query string.
+     *
+     * <p>This method is lightweight. The heavy work will be done in
+     * {@link SearchResults#getNextPageAsync}.
+     *
+     * @param queryExpression query string to search.
+     * @param searchSpec      spec for setting document filters, adding projection, setting term
+     *                        match type, etc.
+     * @return a {@link SearchResults} object for retrieved matched documents.
+     */
+    @NonNull
+    SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * 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
+    ListenableFuture<GetSchemaResponse> getSchemaAsync(@NonNull String packageName,
+            @NonNull String databaseName);
+
+    /**
+     * Returns the {@link Features} to check for the availability of certain features
+     * for this session.
+     */
+    @NonNull
+    Features getFeatures();
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
index 9fb1df0..c89fb1e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
@@ -27,13 +27,25 @@
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface FeatureConstants {
+public final class FeatureConstants {
     /** Feature constants for {@link Features#NUMERIC_SEARCH}. */
-    String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+    public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
 
     /**  Feature constants for {@link Features#VERBATIM_SEARCH}.   */
-    String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+    public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
 
     /**  Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}.  */
-    String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+
+    /**  Feature constants for {@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}.  */
+    public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION =
+            "LIST_FILTER_HAS_PROPERTY_FUNCTION";
+
+    /** A feature constant for the "semanticSearch" function in {@link AppSearchSession#search}. */
+    public static final String EMBEDDING_SEARCH = "EMBEDDING_SEARCH";
+
+    /** A feature constant for the "tokenize" function in {@link AppSearchSession#search}. */
+    public static final String LIST_FILTER_TOKENIZE_FUNCTION = "TOKENIZE";
+
+    private FeatureConstants() {}
 }
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 bfc4deb..e89dc99 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -16,7 +16,8 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
+
+import java.util.Set;
 
 /**
  * A class that encapsulates all features that are only supported in certain cases (e.g. only on
@@ -29,6 +30,8 @@
  */
 
 // @exportToFramework:copyToPath(../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java)
+// Note: When adding new fields, The @RequiresFeature is needed in setters but could be skipped in
+// getters if call the getter won't send unsupported requests to the AppSearch-framework-impl.
 public interface Features {
 
     /**
@@ -60,8 +63,8 @@
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},
-     * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)},
+     * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility(String)},
      * {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()},
      * {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()},
      * {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},
@@ -84,6 +87,7 @@
      * <p>For details on the numeric search expressions in the query language, see
      * {@link AppSearchSession#search}.
      */
+    // Note: The preferred name of this feature should have been LIST_FILTER_NUMERIC_SEARCH.
     String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH;
 
     /**
@@ -94,6 +98,7 @@
      *
      * <p>For details on the verbatim string operator, see {@link AppSearchSession#search}.
      */
+    // Note: The preferred name of this feature should have been LIST_FILTER_VERBATIM_SEARCH.
     String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH;
 
     /**
@@ -106,6 +111,42 @@
     String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE;
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the
+     * "hasProperty" function in query expressions.
+     *
+     * <p>For details on the "hasProperty" function in the query language, see
+     * {@link AppSearchSession#search}.
+     */
+    String LIST_FILTER_HAS_PROPERTY_FUNCTION = FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION;
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the
+     * "tokenize" function in query expressions.
+     *
+     * <p>For details on the "tokenize" function in the query language, see
+     * {@link AppSearchSession#search}.
+     */
+    String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers whether or not the
+     * AppSearch backend can store the descriptions returned by
+     * {@link AppSearchSchema#getDescription} and
+     * {@link AppSearchSchema.PropertyConfig#getDescription}.
+     */
+    String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link AppSearchSchema.EmbeddingPropertyConfig}.
+     *
+     * <p>For details on the embedding search expressions, see {@link AppSearchSession#search} for
+     * the query language and {@link SearchSpec.Builder#setRankingStrategy(String)} for the ranking
+     * language.
+     */
+    String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+
+    /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
      * {@link SearchSpec#GROUPING_TYPE_PER_SCHEMA}
      */
@@ -119,10 +160,9 @@
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link SearchSpec.Builder#addFilterProperties}.
-     * @exportToFramework:hide
+     * {@link SearchSpec.Builder#addFilterProperties} and
+     * {@link SearchSuggestionSpec.Builder#addFilterProperties}.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
 
     /**
@@ -145,12 +185,6 @@
     String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
 
     /**
-     * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link AppSearchSchema.StringPropertyConfig.Builder#setDeletionPropagation}.
-     */
-    String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
-
-    /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers setting schemas with
      * circular references for {@link AppSearchSession#setSchemaAsync}.
      */
@@ -170,6 +204,38 @@
     String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SearchSpec.Builder#setSearchSourceLogTag(String)}.
+     */
+    String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SetSchemaRequest.Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}.
+     */
+    String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig}.
+     */
+    String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG =
+            "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link EnterpriseGlobalSearchSession}
+     */
+    String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SearchSpec.Builder#addInformationalRankingExpressions}.
+     */
+    String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS =
+            "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
+
+    /**
      * Returns whether a feature is supported at run-time. Feature support depends on the
      * feature in question, the AppSearch backend being used and the Android version of the
      * device.
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 57134edba..6de5448 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -17,8 +17,6 @@
 package androidx.appsearch.app;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
-import android.os.Parcelable;
 import android.util.Log;
 
 import androidx.annotation.IntRange;
@@ -27,9 +25,11 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.annotation.Document;
-import androidx.appsearch.app.PropertyPath.PathSegment;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.safeparcel.PropertyParcel;
 import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.core.util.Preconditions;
 
@@ -39,6 +39,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -64,23 +65,9 @@
     /** The maximum number of indexed properties a document can have. */
     private static final int MAX_INDEXED_PROPERTIES = 16;
 
-    /** The default score of document. */
-    private static final int DEFAULT_SCORE = 0;
-
-    /** The default time-to-live in millisecond of a document, which is infinity. */
-    private static final long DEFAULT_TTL_MILLIS = 0L;
-
-    private static final String PROPERTIES_FIELD = "properties";
-    private static final String BYTE_ARRAY_FIELD = "byteArray";
-    private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String ID_FIELD = "id";
-    private static final String SCORE_FIELD = "score";
-    private static final String TTL_MILLIS_FIELD = "ttlMillis";
-    private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
-    private static final String NAMESPACE_FIELD = "namespace";
-    private static final String PARENT_TYPES_FIELD = "parentTypes";
-
     /**
+     * Fixed constant synthetic property for parent types.
+     *
      * <!--@exportToFramework:hide-->
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -104,6 +91,7 @@
      * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}.
      *
      * <!--@exportToFramework:ifJetpack()-->
+     *
      * @deprecated This is no longer a static value, but depends on SDK version and what AppSearch
      * implementation is being used. Use {@link Features#getMaxIndexedProperties} instead.
      * <!--@exportToFramework:else()-->
@@ -138,43 +126,20 @@
     }
 // @exportToFramework:endStrip()
 
-    /**
-     * Contains all {@link GenericDocument} information in a packaged format.
-     *
-     * <p>Keys are the {@code *_FIELD} constants in this class.
-     */
-    @NonNull
-    final Bundle mBundle;
-
-    /** Contains all properties in {@link GenericDocument} to support getting properties via name */
-    @NonNull
-    private final Bundle mProperties;
-
-    @NonNull
-    private final String mId;
-    @NonNull
-    private final String mSchemaType;
-    private final long mCreationTimestampMillis;
-    @Nullable
-    private Integer mHashCode;
+    /** The class to hold all meta data and properties for this {@link GenericDocument}. */
+    private final GenericDocumentParcel mDocumentParcel;
 
     /**
-     * Rebuilds a {@link GenericDocument} from a bundle.
+     * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}.
      *
-     * @param bundle Packaged {@link GenericDocument} data, such as the result of
-     *               {@link #getBundle}.
+     * @param documentParcel Packaged {@link GenericDocument} data, such as the result of
+     *                       {@link #getDocumentParcel()}.
      * @exportToFramework:hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @SuppressWarnings("deprecation")
-    public GenericDocument(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-        mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
-        mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD));
-        mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
-        mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
-                System.currentTimeMillis());
+    public GenericDocument(@NonNull GenericDocumentParcel documentParcel) {
+        mDocumentParcel = Objects.requireNonNull(documentParcel);
     }
 
     /**
@@ -183,36 +148,37 @@
      * <p>This method should be only used by constructor of a subclass.
      */
     protected GenericDocument(@NonNull GenericDocument document) {
-        this(document.mBundle);
+        this(document.mDocumentParcel);
     }
 
     /**
-     * Returns the {@link Bundle} populated by this builder.
+     * Returns the {@link GenericDocumentParcel} holding the values for this
+     * {@link GenericDocument}.
      *
      * @exportToFramework:hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    public GenericDocumentParcel getDocumentParcel() {
+        return mDocumentParcel;
     }
 
     /** Returns the unique identifier of the {@link GenericDocument}. */
     @NonNull
     public String getId() {
-        return mId;
+        return mDocumentParcel.getId();
     }
 
     /** Returns the namespace of the {@link GenericDocument}. */
     @NonNull
     public String getNamespace() {
-        return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
+        return mDocumentParcel.getNamespace();
     }
 
     /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
     @NonNull
     public String getSchemaType() {
-        return mSchemaType;
+        return mDocumentParcel.getSchemaType();
     }
 
     /**
@@ -224,7 +190,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @Nullable
     public List<String> getParentTypes() {
-        List<String> result = mBundle.getStringArrayList(PARENT_TYPES_FIELD);
+        List<String> result = mDocumentParcel.getParentTypes();
         if (result == null) {
             return null;
         }
@@ -238,7 +204,7 @@
      */
     /*@exportToFramework:CurrentTimeMillisLong*/
     public long getCreationTimestampMillis() {
-        return mCreationTimestampMillis;
+        return mDocumentParcel.getCreationTimestampMillis();
     }
 
     /**
@@ -252,7 +218,7 @@
      * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called.
      */
     public long getTtlMillis() {
-        return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
+        return mDocumentParcel.getTtlMillis();
     }
 
     /**
@@ -267,13 +233,13 @@
      * <p>Any non-negative integer can be used a score.
      */
     public int getScore() {
-        return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
+        return mDocumentParcel.getScore();
     }
 
     /** Returns the names of all properties defined in this document. */
     @NonNull
     public Set<String> getPropertyNames() {
-        return Collections.unmodifiableSet(mProperties.keySet());
+        return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames());
     }
 
     /**
@@ -334,61 +300,36 @@
      *
      * @param path The path to look for.
      * @return The entry with the given path as an object or {@code null} if there is no such path.
-     *   The returned object will be one of the following types: {@code String[]}, {@code long[]},
-     *   {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
+     * The returned object will be one of the following types: {@code String[]}, {@code long[]},
+     * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
      */
     @Nullable
     public Object getProperty(@NonNull String path) {
-        Preconditions.checkNotNull(path);
+        Objects.requireNonNull(path);
         Object rawValue =
-                getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/0, mBundle);
+                getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0,
+                        mDocumentParcel.getPropertyMap());
 
         // Unpack the raw value into the types the user expects, if required.
-        if (rawValue instanceof Bundle) {
-            // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance
-            // optimization for lookups.
-            GenericDocument document = new GenericDocument((Bundle) rawValue);
+        if (rawValue instanceof GenericDocumentParcel) {
+            // getRawPropertyFromRawDocument may return a document as a bare documentParcel
+            // as a performance optimization for lookups.
+            GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue);
             return new GenericDocument[]{document};
         }
 
-        if (rawValue instanceof List) {
-            // byte[][] fields are packed into List<Bundle> where each Bundle contains just a single
-            // entry: BYTE_ARRAY_FIELD -> byte[].
-            @SuppressWarnings("unchecked")
-            List<Bundle> bundles = (List<Bundle>) rawValue;
-            byte[][] bytes = new byte[bundles.size()][];
-            for (int i = 0; i < bundles.size(); i++) {
-                Bundle bundle = bundles.get(i);
-                if (bundle == null) {
-                    Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
-                    continue;
-                }
-                byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
-                if (innerBytes == null) {
-                    Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
-                    continue;
-                }
-                bytes[i] = innerBytes;
-            }
-            return bytes;
-        }
-
-        if (rawValue instanceof Parcelable[]) {
-            // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.
+        if (rawValue instanceof GenericDocumentParcel[]) {
+            // The underlying parcelable of nested GenericDocuments is packed into
+            // a Parcelable array.
             // We must unpack it into GenericDocument instances.
-            Parcelable[] bundles = (Parcelable[]) rawValue;
-            GenericDocument[] documents = new GenericDocument[bundles.length];
-            for (int i = 0; i < bundles.length; i++) {
-                if (bundles[i] == null) {
-                    Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+            GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue;
+            GenericDocument[] documents = new GenericDocument[docParcels.length];
+            for (int i = 0; i < docParcels.length; i++) {
+                if (docParcels[i] == null) {
+                    Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path);
                     continue;
                 }
-                if (!(bundles[i] instanceof Bundle)) {
-                    Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass()
-                            + ", not a Bundle for path: " + path);
-                    continue;
-                }
-                documents[i] = new GenericDocument((Bundle) bundles[i]);
+                documents[i] = new GenericDocument(docParcels[i]);
             }
             return documents;
         }
@@ -407,25 +348,21 @@
      * But in the case where we collect documents across repeated nested documents, we need to
      * recurse back into this method, and so we also keep track of the index into the path.
      *
-     * @param path the PropertyPath object representing the path
-     * @param pathIndex the index into the path we start at
-     * @param documentBundle the bundle that contains the path we are looking up
+     * @param path        the PropertyPath object representing the path
+     * @param pathIndex   the index into the path we start at
+     * @param propertyMap the map containing the path we are looking up
      * @return the raw property
      */
     @Nullable
     @SuppressWarnings("deprecation")
     private static Object getRawPropertyFromRawDocument(
-            @NonNull PropertyPath path, int pathIndex, @NonNull Bundle documentBundle) {
-        Preconditions.checkNotNull(path);
-        Preconditions.checkNotNull(documentBundle);
-        Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD));
-
-
+            @NonNull PropertyPath path, int pathIndex,
+            @NonNull Map<String, PropertyParcel> propertyMap) {
+        Objects.requireNonNull(path);
+        Objects.requireNonNull(propertyMap);
         for (int i = pathIndex; i < path.size(); i++) {
-            PathSegment segment = path.get(i);
-
-            Object currentElementValue = properties.get(segment.getPropertyName());
-
+            PropertyPath.PathSegment segment = path.get(i);
+            Object currentElementValue = propertyMap.get(segment.getPropertyName());
             if (currentElementValue == null) {
                 return null;
             }
@@ -435,61 +372,77 @@
             // "recipients[0]", currentElementValue now contains the value of "recipients" while we
             // need the value of "recipients[0]".
             int index = segment.getPropertyIndex();
-            if (index != PathSegment.NON_REPEATED_CARDINALITY) {
+            if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) {
+                // For properties bundle, now we will only get PropertyParcel as the value.
+                PropertyParcel propertyParcel = (PropertyParcel) currentElementValue;
+
                 // Extract the right array element
                 Object extractedValue = null;
-                if (currentElementValue instanceof String[]) {
-                    String[] stringValues = (String[]) currentElementValue;
-                    if (index < stringValues.length) {
+                if (propertyParcel.getStringValues() != null) {
+                    String[] stringValues = propertyParcel.getStringValues();
+                    if (stringValues != null && index < stringValues.length) {
                         extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
                     }
-                } else if (currentElementValue instanceof long[]) {
-                    long[] longValues = (long[]) currentElementValue;
-                    if (index < longValues.length) {
+                } else if (propertyParcel.getLongValues() != null) {
+                    long[] longValues = propertyParcel.getLongValues();
+                    if (longValues != null && index < longValues.length) {
                         extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
                     }
-                } else if (currentElementValue instanceof double[]) {
-                    double[] doubleValues = (double[]) currentElementValue;
-                    if (index < doubleValues.length) {
+                } else if (propertyParcel.getDoubleValues() != null) {
+                    double[] doubleValues = propertyParcel.getDoubleValues();
+                    if (doubleValues != null && index < doubleValues.length) {
                         extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
                     }
-                } else if (currentElementValue instanceof boolean[]) {
-                    boolean[] booleanValues = (boolean[]) currentElementValue;
-                    if (index < booleanValues.length) {
+                } else if (propertyParcel.getBooleanValues() != null) {
+                    boolean[] booleanValues = propertyParcel.getBooleanValues();
+                    if (booleanValues != null && index < booleanValues.length) {
                         extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
                     }
-                } else if (currentElementValue instanceof List) {
-                    @SuppressWarnings("unchecked")
-                    List<Bundle> bundles = (List<Bundle>) currentElementValue;
-                    if (index < bundles.size()) {
-                        extractedValue = bundles.subList(index, index + 1);
+                } else if (propertyParcel.getBytesValues() != null) {
+                    byte[][] bytesValues = propertyParcel.getBytesValues();
+                    if (bytesValues != null && index < bytesValues.length) {
+                        extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1);
                     }
-                } else if (currentElementValue instanceof Parcelable[]) {
+                } else if (propertyParcel.getDocumentValues() != null) {
                     // Special optimization: to avoid creating new singleton arrays for traversing
-                    // paths we return the bare document Bundle in this particular case.
-                    Parcelable[] bundles = (Parcelable[]) currentElementValue;
-                    if (index < bundles.length) {
-                        extractedValue = bundles[index];
+                    // paths we return the bare document parcel in this particular case.
+                    GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();
+                    if (docValues != null && index < docValues.length) {
+                        extractedValue = docValues[index];
+                    }
+                } else if (propertyParcel.getEmbeddingValues() != null) {
+                    EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues();
+                    if (embeddingValues != null && index < embeddingValues.length) {
+                        extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1);
                     }
                 } else {
-                    throw new IllegalStateException("Unsupported value type: "
-                            + currentElementValue);
+                    throw new IllegalStateException(
+                            "Unsupported value type: " + currentElementValue);
                 }
                 currentElementValue = extractedValue;
             }
 
             // at the end of the path, either something like "...foo" or "...foo[1]"
             if (currentElementValue == null || i == path.size() - 1) {
+                if (currentElementValue != null && currentElementValue instanceof PropertyParcel) {
+                    // Unlike previous bundle-based implementation, now each
+                    // value is wrapped in PropertyParcel.
+                    // Here we need to get and return the actual value for non-repeated fields.
+                    currentElementValue = ((PropertyParcel) currentElementValue).getValues();
+                }
                 return currentElementValue;
             }
 
-            // currentElementValue is now a Bundle or Parcelable[], we can continue down the path
-            if (currentElementValue instanceof Bundle) {
-                properties = ((Bundle) currentElementValue).getBundle(PROPERTIES_FIELD);
-            } else if (currentElementValue instanceof Parcelable[]) {
-                Parcelable[] parcelables = (Parcelable[]) currentElementValue;
-                if (parcelables.length == 1) {
-                    properties = ((Bundle) parcelables[0]).getBundle(PROPERTIES_FIELD);
+            // currentElementValue is now a GenericDocumentParcel or PropertyParcel,
+            // we can continue down the path.
+            if (currentElementValue instanceof GenericDocumentParcel) {
+                propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap();
+            } else if (currentElementValue instanceof PropertyParcel
+                    && ((PropertyParcel) currentElementValue).getDocumentValues() != null) {
+                GenericDocumentParcel[] docParcels =
+                        ((PropertyParcel) currentElementValue).getDocumentValues();
+                if (docParcels != null && docParcels.length == 1) {
+                    propertyMap = docParcels[0].getPropertyMap();
                     continue;
                 }
 
@@ -516,17 +469,21 @@
                 // repeated values. The implementation is optimized for these two cases, requiring
                 // no additional allocations. So we've decided that the above performance
                 // characteristics are OK for the less used path.
-                List<Object> accumulator = new ArrayList<>(parcelables.length);
-                for (Parcelable parcelable : parcelables) {
-                    // recurse as we need to branch
-                    Object value = getRawPropertyFromRawDocument(path, /*pathIndex=*/i + 1,
-                            (Bundle) parcelable);
-                    if (value != null) {
-                        accumulator.add(value);
+                if (docParcels != null) {
+                    List<Object> accumulator = new ArrayList<>(docParcels.length);
+                    for (GenericDocumentParcel docParcel : docParcels) {
+                        // recurse as we need to branch
+                        Object value =
+                                getRawPropertyFromRawDocument(
+                                        path, /*pathIndex=*/ i + 1,
+                                        ((GenericDocumentParcel) docParcel).getPropertyMap());
+                        if (value != null) {
+                            accumulator.add(value);
+                        }
                     }
+                    // Break the path traversing loop
+                    return flattenAccumulator(accumulator);
                 }
-                // Break the path traversing loop
-                return flattenAccumulator(accumulator);
             } else {
                 Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
                 return null;
@@ -540,10 +497,10 @@
      * Combines accumulated repeated properties from multiple documents into a single array.
      *
      * @param accumulator List containing objects of the following types: {@code String[]},
-     *                    {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List<Bundle>},
-     *                    or {@code Parcelable[]}.
+     *                    {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]},
+     *                    or {@code GenericDocumentParcelable[]}.
      * @return The result of concatenating each individual list element into a larger array/list of
-     *         the same type.
+     * the same type.
      */
     @Nullable
     private static Object flattenAccumulator(@NonNull List<Object> accumulator) {
@@ -607,28 +564,29 @@
             }
             return result;
         }
-        if (first instanceof List) {
+        if (first instanceof byte[][]) {
             int length = 0;
             for (int i = 0; i < accumulator.size(); i++) {
-                length += ((List<?>) accumulator.get(i)).size();
+                length += ((byte[][]) accumulator.get(i)).length;
             }
-            List<Bundle> result = new ArrayList<>(length);
+            byte[][] result = new byte[length][];
+            int total = 0;
             for (int i = 0; i < accumulator.size(); i++) {
-                @SuppressWarnings("unchecked")
-                List<Bundle> castValue = (List<Bundle>) accumulator.get(i);
-                result.addAll(castValue);
+                byte[][] castValue = (byte[][]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
             }
             return result;
         }
-        if (first instanceof Parcelable[]) {
+        if (first instanceof GenericDocumentParcel[]) {
             int length = 0;
             for (int i = 0; i < accumulator.size(); i++) {
-                length += ((Parcelable[]) accumulator.get(i)).length;
+                length += ((GenericDocumentParcel[]) accumulator.get(i)).length;
             }
-            Parcelable[] result = new Parcelable[length];
+            GenericDocumentParcel[] result = new GenericDocumentParcel[length];
             int total = 0;
             for (int i = 0; i < accumulator.size(); i++) {
-                Parcelable[] castValue = (Parcelable[]) accumulator.get(i);
+                GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i);
                 System.arraycopy(castValue, 0, result, total, castValue.length);
                 total += castValue.length;
             }
@@ -754,6 +712,27 @@
         return propertyArray[0];
     }
 
+    /**
+     * Retrieves an {@code EmbeddingVector} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code EmbeddingVector[]} associated with the given path or
+     * {@code null} if there is no such value or the value is of a different type.
+     */
+    @Nullable
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public EmbeddingVector getPropertyEmbedding(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return null;
+        }
+        warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length);
+        return propertyArray[0];
+    }
+
     /** Prints a warning to logcat if the given propertyLength is greater than 1. */
     private static void warnIfSinglePropertyTooLong(
             @NonNull String propertyType, @NonNull String path, int propertyLength) {
@@ -862,13 +841,13 @@
      * returns {@code null}.
      *
      * <!--@exportToFramework:ifJetpack()-->
-     *   <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
-     *   this method returns an empty {@code byte[][]}.
+     * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+     * this method returns an empty {@code byte[][]}.
      * <!--@exportToFramework:else()
-     *   <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
-     *   this method returns an empty {@code byte[][]} starting in
-     *   {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
-     *   versions of Android.
+     * <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+     * this method returns an empty {@code byte[][]} starting in
+     * {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
+     * versions of Android.
      * -->
      *
      * @param path The path to look for.
@@ -892,13 +871,13 @@
      * returns {@code null}.
      *
      * <!--@exportToFramework:ifJetpack()-->
-     *   <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
-     *   {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
+     * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+     * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
      * <!--@exportToFramework:else()
-     *   <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
-     *   {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
-     *   in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
-     *   versions of Android.
+     * <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+     * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
+     * in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
+     * versions of Android.
      * -->
      *
      * @param path The path to look for.
@@ -914,11 +893,36 @@
     }
 
     /**
+     * Retrieves a repeated {@code EmbeddingVector[]} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty
+     * {@code EmbeddingVector[]}, this method returns an empty
+     * {@code EmbeddingVector[]}.
+     *
+     * @param path The path to look for.
+     * @return The {@code EmbeddingVector[]} associated with the given path, or
+     * {@code null} if no value is set or the value is of a different type.
+     */
+    @SuppressLint({"ArrayReturn", "NullableCollection"})
+    @Nullable
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, EmbeddingVector[].class);
+    }
+
+    /**
      * Casts a repeated property to the provided type, logging an error and returning {@code null}
      * if the cast fails.
      *
-     * @param path Path to the property within the document. Used for logging.
-     * @param value Value of the property
+     * @param path   Path to the property within the document. Used for logging.
+     * @param value  Value of the property
      * @param tClass Class to cast the value into
      */
     @Nullable
@@ -1056,6 +1060,7 @@
      * {@link GenericDocument.Builder}.
      *
      * <p>The returned builder is a deep copy whose data is separate from this document.
+     *
      * @deprecated This API is not compliant with API guidelines.
      * Use {@link Builder#Builder(GenericDocument)} instead.
      * <!--@exportToFramework:hide-->
@@ -1064,8 +1069,7 @@
     @NonNull
     @Deprecated
     public GenericDocument.Builder<GenericDocument.Builder<?>> toBuilder() {
-        Bundle clonedBundle = BundleUtil.deepCopy(mBundle);
-        return new GenericDocument.Builder<>(clonedBundle);
+        return new Builder<>(new GenericDocumentParcel.Builder(mDocumentParcel));
     }
 
     @Override
@@ -1077,15 +1081,12 @@
             return false;
         }
         GenericDocument otherDocument = (GenericDocument) other;
-        return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
+        return mDocumentParcel.equals(otherDocument.mDocumentParcel);
     }
 
     @Override
     public int hashCode() {
-        if (mHashCode == null) {
-            mHashCode = BundleUtil.deepHashCode(mBundle);
-        }
-        return mHashCode;
+        return mDocumentParcel.hashCode();
     }
 
     @Override
@@ -1099,7 +1100,7 @@
     /**
      * Appends a debug string for the {@link GenericDocument} instance to the given string builder.
      *
-     * @param builder     the builder to append to.
+     * @param builder the builder to append to.
      */
     void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) {
         Preconditions.checkNotNull(builder);
@@ -1147,9 +1148,9 @@
     /**
      * Appends a debug string for the given document property to the given string builder.
      *
-     * @param propertyName  name of property to create string for.
-     * @param property      property object to create string for.
-     * @param builder       the builder to append to.
+     * @param propertyName name of property to create string for.
+     * @param property     property object to create string for.
+     * @param builder      the builder to append to.
      */
     private void appendPropertyString(@NonNull String propertyName, @NonNull Object property,
             @NonNull IndentingStringBuilder builder) {
@@ -1178,7 +1179,7 @@
                     builder.append("\"").append((String) propertyElement).append("\"");
                 } else if (propertyElement instanceof byte[]) {
                     builder.append(Arrays.toString((byte[]) propertyElement));
-                } else {
+                } else if (propertyElement != null) {
                     builder.append(propertyElement.toString());
                 }
                 if (i != propertyArrLength - 1) {
@@ -1197,11 +1198,10 @@
     // This builder is specifically designed to be extended by classes deriving from
     // GenericDocument.
     @SuppressLint("StaticFinalBuilder")
+    @SuppressWarnings("rawtypes")
     public static class Builder<BuilderType extends Builder> {
-        private Bundle mBundle;
-        private Bundle mProperties;
+        private final GenericDocumentParcel.Builder mDocumentParcelBuilder;
         private final BuilderType mBuilderTypeInstance;
-        private boolean mBuilt = false;
 
         /**
          * Creates a new {@link GenericDocument.Builder}.
@@ -1228,41 +1228,31 @@
             Preconditions.checkNotNull(id);
             Preconditions.checkNotNull(schemaType);
 
-            mBundle = new Bundle();
             mBuilderTypeInstance = (BuilderType) this;
-            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
-            mBundle.putString(GenericDocument.ID_FIELD, id);
-            mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
-            mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
-            mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);
-
-            mProperties = new Bundle();
-            mBundle.putBundle(PROPERTIES_FIELD, mProperties);
+            mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType);
         }
 
         /**
-         * Creates a new {@link GenericDocument.Builder} from the given Bundle.
+         * Creates a new {@link GenericDocument.Builder} from the given
+         * {@link GenericDocumentParcel.Builder}.
          *
          * <p>The bundle is NOT copied.
          */
         @SuppressWarnings("unchecked")
-        Builder(@NonNull Bundle bundle) {
-            mBundle = Preconditions.checkNotNull(bundle);
-            // mProperties is NonNull and initialized to empty Bundle() in builder.
-            mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));
+        Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) {
+            mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder);
             mBuilderTypeInstance = (BuilderType) this;
         }
 
         /**
          * Creates a new {@link GenericDocument.Builder} from the given GenericDocument.
          *
-         * <p>The GenericDocument is deep copied, i.e. changes to the new GenericDocument
-         * returned by this function will NOT affect the original GenericDocument.
-         * <!--@exportToFramework:hide-->
+         * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument
+         * returned by this function and will NOT affect the original GenericDocument.
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR)
         public Builder(@NonNull GenericDocument document) {
-            this(BundleUtil.deepCopy(document.getBundle()));
+            this(new GenericDocumentParcel.Builder(document.mDocumentParcel));
         }
 
         /**
@@ -1272,14 +1262,13 @@
          * <p>Document IDs are unique within a namespace.
          *
          * <p>The number of namespaces per app should be kept small for efficiency reasons.
-         * <!--@exportToFramework:hide-->
          */
+        @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
         @CanIgnoreReturnValue
         @NonNull
         public BuilderType setNamespace(@NonNull String namespace) {
             Preconditions.checkNotNull(namespace);
-            resetIfBuilt();
-            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+            mDocumentParcelBuilder.setNamespace(namespace);
             return mBuilderTypeInstance;
         }
 
@@ -1287,15 +1276,17 @@
          * Sets the ID of this document, changing the value provided in the constructor. No
          * special values are reserved or understood by the infrastructure.
          *
-         * <p>Document IDs are unique within a namespace.
-         * <!--@exportToFramework:hide-->
+         * <p>Document IDs are unique within the combination of package, database, and namespace.
+         *
+         * <p>Setting a document with a duplicate id will overwrite the original document with
+         * the new document, enforcing uniqueness within the above constraint.
          */
+        @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
         @CanIgnoreReturnValue
         @NonNull
         public BuilderType setId(@NonNull String id) {
             Preconditions.checkNotNull(id);
-            resetIfBuilt();
-            mBundle.putString(GenericDocument.ID_FIELD, id);
+            mDocumentParcelBuilder.setId(id);
             return mBuilderTypeInstance;
         }
 
@@ -1303,15 +1294,15 @@
          * 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#setSchemaAsync}.
-         * <!--@exportToFramework:hide-->
+         * {@link AppSearchSchema} object previously provided to
+         * {@link AppSearchSession#setSchemaAsync}.
          */
+        @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
         @CanIgnoreReturnValue
         @NonNull
         public BuilderType setSchemaType(@NonNull String schemaType) {
             Preconditions.checkNotNull(schemaType);
-            resetIfBuilt();
-            mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
+            mDocumentParcelBuilder.setSchemaType(schemaType);
             return mBuilderTypeInstance;
         }
 
@@ -1326,9 +1317,7 @@
         @NonNull
         public BuilderType setParentTypes(@NonNull List<String> parentTypes) {
             Preconditions.checkNotNull(parentTypes);
-            resetIfBuilt();
-            mBundle.putStringArrayList(GenericDocument.PARENT_TYPES_FIELD,
-                    new ArrayList<>(parentTypes));
+            mDocumentParcelBuilder.setParentTypes(parentTypes);
             return mBuilderTypeInstance;
         }
 
@@ -1352,8 +1341,7 @@
             if (score < 0) {
                 throw new IllegalArgumentException("Document score cannot be negative.");
             }
-            resetIfBuilt();
-            mBundle.putInt(GenericDocument.SCORE_FIELD, score);
+            mDocumentParcelBuilder.setScore(score);
             return mBuilderTypeInstance;
         }
 
@@ -1371,9 +1359,7 @@
         @NonNull
         public BuilderType setCreationTimestampMillis(
                 /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
-            resetIfBuilt();
-            mBundle.putLong(
-                    GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis);
+            mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis);
             return mBuilderTypeInstance;
         }
 
@@ -1397,8 +1383,7 @@
             if (ttlMillis < 0) {
                 throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
             }
-            resetIfBuilt();
-            mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
+            mDocumentParcelBuilder.setTtlMillis(ttlMillis);
             return mBuilderTypeInstance;
         }
 
@@ -1406,9 +1391,9 @@
          * Sets one or multiple {@code String} values for a property, replacing its previous
          * values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@code String} values of the property.
          * @throws IllegalArgumentException if no values are provided, or if a passed in
          *                                  {@code String} is {@code null} or "".
@@ -1418,8 +1403,13 @@
         public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The String at " + i + " is null.");
+                }
+            }
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -1427,9 +1417,9 @@
          * Sets one or multiple {@code boolean} values for a property, replacing its previous
          * values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@code boolean} values of the property.
          * @throws IllegalArgumentException if the name is empty or {@code null}.
          */
@@ -1438,8 +1428,8 @@
         public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -1447,9 +1437,9 @@
          * Sets one or multiple {@code long} values for a property, replacing its previous
          * values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@code long} values of the property.
          * @throws IllegalArgumentException if the name is empty or {@code null}.
          */
@@ -1458,8 +1448,8 @@
         public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -1467,9 +1457,9 @@
          * Sets one or multiple {@code double} values for a property, replacing its previous
          * values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@code double} values of the property.
          * @throws IllegalArgumentException if the name is empty or {@code null}.
          */
@@ -1478,17 +1468,17 @@
         public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
         /**
          * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@code byte[]} of the property.
          * @throws IllegalArgumentException if no values are provided, or if a passed in
          *                                  {@code byte[]} is {@code null}, or if name is empty.
@@ -1498,8 +1488,13 @@
         public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The byte[] at " + i + " is null.");
+                }
+            }
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -1507,9 +1502,9 @@
          * Sets one or multiple {@link GenericDocument} values for a property, replacing its
          * previous values.
          *
-         * @param name    the name associated with the {@code values}. Must match the name
-         *                for this property as given in
-         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
          * @param values the {@link GenericDocument} values of the property.
          * @throws IllegalArgumentException if no values are provided, or if a passed in
          *                                  {@link GenericDocument} is {@code null}, or if name
@@ -1521,8 +1516,43 @@
                 @NonNull String name, @NonNull GenericDocument... values) {
             Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            resetIfBuilt();
-            putInPropertyBundle(name, values);
+            validatePropertyName(name);
+            GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length];
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The document at " + i + " is null.");
+                }
+                documentParcels[i] = values[i].getDocumentParcel();
+            }
+            mDocumentParcelBuilder.putInPropertyMap(name, documentParcels);
+            return mBuilderTypeInstance;
+        }
+
+        /**
+         * Sets one or multiple {@code EmbeddingVector} values for a property, replacing
+         * its previous values.
+         *
+         * @param name   the name associated with the {@code values}. Must match the name
+         *               for this property as given in
+         *               {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code EmbeddingVector} values of the property.
+         * @throws IllegalArgumentException if the name is empty or {@code null}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public BuilderType setPropertyEmbedding(@NonNull String name,
+                @NonNull EmbeddingVector... values) {
+            Preconditions.checkNotNull(name);
+            Preconditions.checkNotNull(values);
+            validatePropertyName(name);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException(
+                            "The EmbeddingVector at " + i + " is null.");
+                }
+            }
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -1531,95 +1561,27 @@
          *
          * <p>Note that this method does not support property paths.
          *
+         * <p>You should check for the existence of the property in {@link #getPropertyNames} if
+         * you need to make sure the property being cleared actually exists.
+         *
+         * <p>If the string passed is an invalid or nonexistent property, no error message or
+         * behavior will be observed.
+         *
          * @param name The name of the property to clear.
-         * <!--@exportToFramework:hide-->
          */
+        @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS)
         @CanIgnoreReturnValue
         @NonNull
         public BuilderType clearProperty(@NonNull String name) {
             Preconditions.checkNotNull(name);
-            resetIfBuilt();
-            mProperties.remove(name);
+            mDocumentParcelBuilder.clearProperty(name);
             return mBuilderTypeInstance;
         }
 
-        private void putInPropertyBundle(@NonNull String name, @NonNull String[] values)
-                throws IllegalArgumentException {
-            validatePropertyName(name);
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] == null) {
-                    throw new IllegalArgumentException("The String at " + i + " is null.");
-                }
-            }
-            mProperties.putStringArray(name, values);
-        }
-
-        private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {
-            validatePropertyName(name);
-            mProperties.putBooleanArray(name, values);
-        }
-
-        private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {
-            validatePropertyName(name);
-            mProperties.putDoubleArray(name, values);
-        }
-
-        private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {
-            validatePropertyName(name);
-            mProperties.putLongArray(name, values);
-        }
-
-        /**
-         * Converts and saves a byte[][] into {@link #mProperties}.
-         *
-         * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
-         * into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
-         */
-        private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {
-            validatePropertyName(name);
-            ArrayList<Bundle> bundles = new ArrayList<>(values.length);
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] == null) {
-                    throw new IllegalArgumentException("The byte[] at " + i + " is null.");
-                }
-                Bundle bundle = new Bundle();
-                bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
-                bundles.add(bundle);
-            }
-            mProperties.putParcelableArrayList(name, bundles);
-        }
-
-        private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {
-            validatePropertyName(name);
-            Parcelable[] documentBundles = new Parcelable[values.length];
-            for (int i = 0; i < values.length; i++) {
-                if (values[i] == null) {
-                    throw new IllegalArgumentException("The document at " + i + " is null.");
-                }
-                documentBundles[i] = values[i].mBundle;
-            }
-            mProperties.putParcelableArray(name, documentBundles);
-        }
-
         /** Builds the {@link GenericDocument} object. */
         @NonNull
         public GenericDocument build() {
-            mBuilt = true;
-            // Set current timestamp for creation timestamp by default.
-            if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) {
-                mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
-                        System.currentTimeMillis());
-            }
-            return new GenericDocument(mBundle);
-        }
-
-        private void resetIfBuilt() {
-            if (mBuilt) {
-                mBundle = BundleUtil.deepCopy(mBundle);
-                // mProperties is NonNull and initialized to empty Bundle() in builder.
-                mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD));
-                mBuilt = false;
-            }
+            return new GenericDocument(mDocumentParcelBuilder.build());
         }
 
         /** Method to ensure property names are not blank */
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 ee59e34..be17430 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -16,9 +16,20 @@
 
 package androidx.appsearch.app;
 
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetByDocumentIdRequestCreator;
+import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -29,6 +40,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -37,7 +49,13 @@
  *
  * @see AppSearchSession#getByDocumentIdAsync
  */
-public final class GetByDocumentIdRequest {
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "GetByDocumentIdRequestCreator")
+public final class GetByDocumentIdRequest extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<GetByDocumentIdRequest> CREATOR =
+            new GetByDocumentIdRequestCreator();
     /**
      * Schema type to be used in
      * {@link GetByDocumentIdRequest.Builder#addProjection}
@@ -45,15 +63,30 @@
      * property paths set.
      */
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
-    private final String mNamespace;
-    private final Set<String> mIds;
-    private final Map<String, List<String>> mTypePropertyPathsMap;
 
-    GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set<String> ids, @NonNull Map<String,
-            List<String>> typePropertyPathsMap) {
-        mNamespace = Preconditions.checkNotNull(namespace);
-        mIds = Preconditions.checkNotNull(ids);
-        mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
+    private final String mNamespace;
+    @NonNull
+    @Field(id = 2)
+    final List<String> mIds;
+    @NonNull
+    @Field(id = 3)
+    final Bundle mTypePropertyPaths;
+
+    /**
+     * Cache of the ids. Comes from inflating mIds at first use.
+     */
+    @Nullable private Set<String> mIdsCached;
+
+    @Constructor
+    GetByDocumentIdRequest(
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull List<String> ids,
+            @Param(id = 3) @NonNull Bundle typePropertyPaths) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mIds = Objects.requireNonNull(ids);
+        mTypePropertyPaths = Objects.requireNonNull(typePropertyPaths);
     }
 
     /** Returns the namespace attached to the request. */
@@ -65,7 +98,10 @@
     /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getIds() {
-        return Collections.unmodifiableSet(mIds);
+        if (mIdsCached == null) {
+            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+        }
+        return mIdsCached;
     }
 
     /**
@@ -78,11 +114,15 @@
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
-        Map<String, List<String>> copy = new ArrayMap<>();
-        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
-            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        Set<String> schemas = mTypePropertyPaths.keySet();
+        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List<String> propertyPaths = mTypePropertyPaths.getStringArrayList(schema);
+            if (propertyPaths != null) {
+                typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPaths));
+            }
         }
-        return copy;
+        return typePropertyPathsMap;
     }
 
     /**
@@ -95,38 +135,34 @@
      */
     @NonNull
     public Map<String, List<PropertyPath>> getProjectionPaths() {
-        Map<String, List<PropertyPath>> copy = new ArrayMap<>(mTypePropertyPathsMap.size());
-        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
-            List<PropertyPath> propertyPathList = new ArrayList<>(entry.getValue().size());
-            for (String p: entry.getValue()) {
-                propertyPathList.add(new PropertyPath(p));
+        Set<String> schemas = mTypePropertyPaths.keySet();
+        Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List<String> paths = mTypePropertyPaths.getStringArrayList(schema);
+            if (paths != null) {
+                int pathsSize = paths.size();
+                List<PropertyPath> propertyPathList = new ArrayList<>(pathsSize);
+                for (int i = 0; i < pathsSize; i++) {
+                    propertyPathList.add(new PropertyPath(paths.get(i)));
+                }
+                typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPathList));
             }
-            copy.put(entry.getKey(), propertyPathList);
         }
-        return copy;
+        return typePropertyPathsMap;
     }
 
-    /**
-     * Returns a map from schema type to property paths to be used for projection.
-     *
-     * <p>If the map is empty, then all properties will be retrieved for all results.
-     *
-     * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map.
-     * This is not meant to be unhidden and should only be used by internal classes.
-     *
-     * @exportToFramework:hide
-     */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Map<String, List<String>> getProjectionsInternal() {
-        return mTypePropertyPathsMap;
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        GetByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link GetByDocumentIdRequest} objects. */
     public static final class Builder {
         private final String mNamespace;
-        private ArraySet<String> mIds = new ArraySet<>();
-        private ArrayMap<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+        private List<String> mIds = new ArrayList<>();
+        private Bundle mProjectionTypePropertyPaths = new Bundle();
         private boolean mBuilt = false;
 
         /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
@@ -176,12 +212,12 @@
             Preconditions.checkNotNull(schemaType);
             Preconditions.checkNotNull(propertyPaths);
             resetIfBuilt();
-            List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+            ArrayList<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
             for (String propertyPath : propertyPaths) {
                 Preconditions.checkNotNull(propertyPath);
                 propertyPathsList.add(propertyPath);
             }
-            mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+            mProjectionTypePropertyPaths.putStringArrayList(schemaType, propertyPathsList);
             return this;
         }
 
@@ -223,11 +259,11 @@
 
         private void resetIfBuilt() {
             if (mBuilt) {
-                mIds = new ArraySet<>(mIds);
+                mIds = new ArrayList<>(mIds);
                 // No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since
                 // the builder only replaces it, never adds to it. So even if the builder is used
                 // again, the previous one will remain with the object.
-                mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);
+                mProjectionTypePropertyPaths = BundleUtil.deepCopy(mProjectionTypePropertyPaths);
                 mBuilt = false;
             }
         }
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 0933654..74f370e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -17,7 +17,8 @@
 package androidx.appsearch.app;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
@@ -25,6 +26,11 @@
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetSchemaResponseCreator;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -36,58 +42,88 @@
 import java.util.Set;
 
 /** 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";
[email protected](creator = "GetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class GetSchemaResponse extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<GetSchemaResponse> CREATOR =
+            new GetSchemaResponseCreator();
+
+    @Field(id = 1, getter = "getVersion")
+    private final int mVersion;
+
+    @Field(id = 2)
+    final List<AppSearchSchema> mSchemas;
+
+    /**
+     * List of VisibilityConfigs for the current schema. May be {@code null} if retrieving the
+     * visibility settings is not possible on the current backend.
+     */
+    @Field(id = 3)
+    @Nullable
+    final List<InternalVisibilityConfig> mVisibilityConfigs;
+
+    /**
+     * This set contains all schemas most recently successfully provided to
+     * {@link AppSearchSession#setSchemaAsync}. We do lazy fetch, the object will be created when
+     * you first time fetch it.
+     */
+    @Nullable
+    private Set<AppSearchSchema> mSchemasCached;
+
     /**
      * 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 you first time fetch it.
      */
     @Nullable
-    private Set<String> mSchemasNotDisplayedBySystem;
+    private Set<String> mSchemasNotDisplayedBySystemCached;
+
     /**
      * 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 you first time fetch it.
      */
     @Nullable
-    private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+    private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackagesCached;
 
     /**
      * 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 you 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
+     * objects under the {@code schemaType} if they hold ALL required permissions of ANY
      * combinations.
-     * The value set represents
-     * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
+     * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
      */
     @Nullable
-    private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;
-
-    private final Bundle mBundle;
-
-    GetSchemaResponse(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
+    private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissionsCached;
 
     /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
+     * This map contains all publicly visible schemas and the {@link PackageIdentifier} specifying
+     * the package that the schemas are from.
      */
-    @NonNull
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public Bundle getBundle() {
-        return mBundle;
+    @Nullable
+    private Map<String, PackageIdentifier> mPubliclyVisibleSchemasCached;
+
+    /**
+     * This map contains all {@link SchemaVisibilityConfig}s 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 you first time fetch it.
+     */
+    @Nullable
+    private Map<String, Set<SchemaVisibilityConfig>> mSchemasVisibleToConfigsCached;
+
+    @Constructor
+    GetSchemaResponse(
+            @Param(id = 1) int version,
+            @Param(id = 2) @NonNull List<AppSearchSchema> schemas,
+            @Param(id = 3) @Nullable List<InternalVisibilityConfig> visibilityConfigs) {
+        mVersion = version;
+        mSchemas = Preconditions.checkNotNull(schemas);
+        mVisibilityConfigs = visibilityConfigs;
     }
 
     /**
@@ -97,25 +133,19 @@
      */
     @IntRange(from = 0)
     public int getVersion() {
-        return mBundle.getInt(VERSION_FIELD);
+        return mVersion;
     }
 
     /**
      * Return the schemas most recently successfully provided to
      * {@link AppSearchSession#setSchemaAsync}.
-     *
-     * <p>It is inefficient to call this method repeatedly.
      */
     @NonNull
-    @SuppressWarnings("deprecation")
     public Set<AppSearchSchema> getSchemas() {
-        ArrayList<Bundle> schemaBundles = Preconditions.checkNotNull(
-                    mBundle.getParcelableArrayList(SCHEMAS_FIELD));
-        Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
-        for (int i = 0; i < schemaBundles.size(); i++) {
-            schemas.add(new AppSearchSchema(schemaBundles.get(i)));
+        if (mSchemasCached == null) {
+            mSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mSchemas));
         }
-        return schemas;
+        return mSchemasCached;
     }
 
     /**
@@ -126,21 +156,22 @@
      * called with false.
      * <!--@exportToFramework:else()-->
      */
-    // @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));
+        List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+        if (mSchemasNotDisplayedBySystemCached == null) {
+            Set<String> copy = new ArraySet<>();
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                if (visibilityConfigs.get(i).isNotDisplayedBySystem()) {
+                    copy.add(visibilityConfigs.get(i).getSchemaType());
+                }
+            }
+            mSchemasNotDisplayedBySystemCached = Collections.unmodifiableSet(copy);
         }
-        return mSchemasNotDisplayedBySystem;
+        return mSchemasNotDisplayedBySystemCached;
     }
 
     /**
@@ -151,37 +182,32 @@
      * called with false.
      * <!--@exportToFramework:else()-->
      */
-    // @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 = Preconditions.checkNotNull(
-                        mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));
+        List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+        if (mSchemasVisibleToPackagesCached == null) {
             Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
-            for (String key : schemaVisibleToPackagesBundle.keySet()) {
-                List<Bundle> PackageIdentifierBundles = Preconditions.checkNotNull(
-                            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)));
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+                List<PackageIdentifier> visibleToPackages =
+                        visibilityConfig.getVisibilityConfig().getAllowedPackages();
+                if (!visibleToPackages.isEmpty()) {
+                    copy.put(
+                            visibilityConfig.getSchemaType(),
+                            Collections.unmodifiableSet(new ArraySet<>(visibleToPackages)));
                 }
-                copy.put(key, packageIdentifiers);
             }
-            mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
+            mSchemasVisibleToPackagesCached = Collections.unmodifiableMap(copy);
         }
-        return mSchemasVisibleToPackages;
+        return mSchemasVisibleToPackagesCached;
     }
 
     /**
-     * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
-     * combinations that querier must hold to access that schema type.
+     * Returns a mapping of schema types to the set of {@link android.Manifest.permission}
+     * combination sets 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.
@@ -189,12 +215,12 @@
      * <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>
+     *     <li>A querier holding both PermissionA and PermissionB has access.</li>
+     *     <li>A querier holding both PermissionC and PermissionD has access.</li>
+     *     <li>A querier holding only PermissionE has access.</li>
+     *     <li>A querier holding both PermissionA and PermissionE has access.</li>
+     *     <li>A querier holding only PermissionA doesn't have access.</li>
+     *     <li>A querier holding only PermissionA and PermissionC doesn't have access.</li>
      * </ul>
      *
      * @return The map contains schema type and all combinations of required permission for querier
@@ -208,56 +234,118 @@
      * called with false.
      * <!--@exportToFramework:else()-->
      */
-    // @exportToFramework:startStrip()
+    // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
     @RequiresFeature(
             enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
             name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
-    // @exportToFramework:endStrip()
     @NonNull
-    @SuppressWarnings("deprecation")
     public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
-        checkGetVisibilitySettingSupported();
-        if (mSchemasVisibleToPermissions == null) {
+        List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+        if (mSchemasVisibleToPermissionsCached == null) {
             Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
-            Bundle schemaVisibleToPermissionBundle = Preconditions.checkNotNull(
-                        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)));
-                    }
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+                Set<Set<Integer>> visibleToPermissions =
+                        visibilityConfig.getVisibilityConfig().getRequiredPermissions();
+                if (!visibleToPermissions.isEmpty()) {
+                    copy.put(
+                            visibilityConfig.getSchemaType(),
+                            Collections.unmodifiableSet(visibleToPermissions));
                 }
-                copy.put(key, visibleToPermissions);
             }
-            mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
+            mSchemasVisibleToPermissionsCached = Collections.unmodifiableMap(copy);
         }
-        return mSchemasVisibleToPermissions;
+        return mSchemasVisibleToPermissionsCached;
     }
 
-    private void checkGetVisibilitySettingSupported() {
-        if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
+    /**
+     * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying
+     * the package the schemas are from.
+     *
+     * <p> If no schemas have been set as publicly visible, an empty set will be returned.
+     * <!--@exportToFramework:ifJetpack()-->
+     * @throws UnsupportedOperationException if {@link Builder#setVisibilitySettingSupported} was
+     * called with false.
+     * <!--@exportToFramework:else()-->
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    @NonNull
+    public Map<String, PackageIdentifier> getPubliclyVisibleSchemas() {
+        List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+        if (mPubliclyVisibleSchemasCached == null) {
+            Map<String, PackageIdentifier> copy = new ArrayMap<>();
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+                PackageIdentifier publiclyVisibleTargetPackage =
+                        visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage();
+                if (publiclyVisibleTargetPackage != null) {
+                    copy.put(visibilityConfig.getSchemaType(), publiclyVisibleTargetPackage);
+                }
+            }
+            mPubliclyVisibleSchemasCached = Collections.unmodifiableMap(copy);
+        }
+        return mPubliclyVisibleSchemasCached;
+    }
+
+    /**
+     * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
+     * access to that schema type.
+     *
+     * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+    @RequiresFeature(
+            enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+            name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
+    @NonNull
+    public Map<String, Set<SchemaVisibilityConfig>> getSchemaTypesVisibleToConfigs() {
+        List<InternalVisibilityConfig> visibilityConfigs = getVisibilityConfigsOrThrow();
+        if (mSchemasVisibleToConfigsCached == null) {
+            Map<String, Set<SchemaVisibilityConfig>> copy = new ArrayMap<>();
+            for (int i = 0; i < visibilityConfigs.size(); i++) {
+                InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i);
+                Set<SchemaVisibilityConfig> nestedVisibilityConfigs =
+                        visibilityConfig.getVisibleToConfigs();
+                if (!nestedVisibilityConfigs.isEmpty()) {
+                    copy.put(visibilityConfig.getSchemaType(),
+                            Collections.unmodifiableSet(nestedVisibilityConfigs));
+                }
+            }
+            mSchemasVisibleToConfigsCached = Collections.unmodifiableMap(copy);
+        }
+        return mSchemasVisibleToConfigsCached;
+    }
+
+    @NonNull
+    private List<InternalVisibilityConfig> getVisibilityConfigsOrThrow() {
+        List<InternalVisibilityConfig> visibilityConfigs = mVisibilityConfigs;
+        if (visibilityConfigs == null) {
             throw new UnsupportedOperationException("Get visibility setting is not supported with "
                     + "this backend/Android API level combination.");
         }
+        return visibilityConfigs;
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        GetSchemaResponseCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link GetSchemaResponse} objects. */
     public static final class Builder {
         private int mVersion = 0;
-        private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+        private ArrayList<AppSearchSchema> mSchemas = 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 Map<String, InternalVisibilityConfig.Builder> mVisibilityConfigBuilders;
         private boolean mBuilt = false;
 
         /** Create a {@link Builder} object} */
@@ -284,7 +372,7 @@
         public Builder addSchema(@NonNull AppSearchSchema schema) {
             Preconditions.checkNotNull(schema);
             resetIfBuilt();
-            mSchemaBundles.add(schema.getBundle());
+            mSchemas.add(schema);
             return this;
         }
 
@@ -302,10 +390,9 @@
         public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
             Preconditions.checkNotNull(schemaType);
             resetIfBuilt();
-            if (mSchemasNotDisplayedBySystem == null) {
-                mSchemasNotDisplayedBySystem = new ArrayList<>();
-            }
-            mSchemasNotDisplayedBySystem.add(schemaType);
+            InternalVisibilityConfig.Builder visibilityConfigBuilder =
+                    getOrCreateVisibilityConfigBuilder(schemaType);
+            visibilityConfigBuilder.setNotDisplayedBySystem(true);
             return this;
         }
 
@@ -337,11 +424,11 @@
             Preconditions.checkNotNull(schemaType);
             Preconditions.checkNotNull(packageIdentifiers);
             resetIfBuilt();
-            ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
+            InternalVisibilityConfig.Builder visibilityConfigBuilder =
+                    getOrCreateVisibilityConfigBuilder(schemaType);
             for (PackageIdentifier packageIdentifier : packageIdentifiers) {
-                bundles.add(packageIdentifier.getBundle());
+                visibilityConfigBuilder.addVisibleToPackage(packageIdentifier);
             }
-            mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
             return this;
         }
 
@@ -364,41 +451,103 @@
          *     <li>A querier holds both PermissionA and PermissionC doesn't have access.</li>
          * </ul>
          *
+         * @param schemaType              The schema type to set visibility on.
+         * @param visibleToPermissionSets The Sets of Android permissions that will be required to
+         *                                access the given schema.
          * @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.
          */
+        // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
         // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
         @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
+        // @SetSchemaRequest is an IntDef annotation applied to Set<Set<Integer>>.
+        @SuppressWarnings("SupportAnnotationUsage")
         @NonNull
         public Builder setRequiredPermissionsForSchemaTypeVisibility(
                 @NonNull String schemaType,
                 @SetSchemaRequest.AppSearchSupportedPermission @NonNull
-                        Set<Set<Integer>> visibleToPermissions) {
+                        Set<Set<Integer>> visibleToPermissionSets) {
             Preconditions.checkNotNull(schemaType);
-            Preconditions.checkNotNull(visibleToPermissions);
+            Preconditions.checkNotNull(visibleToPermissionSets);
             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);
+            InternalVisibilityConfig.Builder visibilityConfigBuilder =
+                    getOrCreateVisibilityConfigBuilder(schemaType);
+            for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+                visibilityConfigBuilder.addVisibleToPermissions(visibleToPermissions);
             }
-            mSchemasVisibleToPermissions.putParcelableArrayList(schemaType,
-                    visibleToPermissionsBundle);
+            return this;
+        }
+
+        /**
+         * Specify that the schema should be publicly available, to packages which already have
+         * visibility to {@code packageIdentifier}.
+         *
+         * @param schemaType the schema to make publicly accessible.
+         * @param packageIdentifier the package from which the document schema is from.
+         * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema
+         */
+        // Merged list available from getPubliclyVisibleSchemas
+        @CanIgnoreReturnValue
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+        @NonNull
+        public Builder setPubliclyVisibleSchema(
+                @NonNull String schemaType, @NonNull PackageIdentifier packageIdentifier) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(packageIdentifier);
+            resetIfBuilt();
+            InternalVisibilityConfig.Builder visibilityConfigBuilder =
+                    getOrCreateVisibilityConfigBuilder(schemaType);
+            visibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);
+            return this;
+        }
+
+        /**
+         * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+         * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+         *
+         * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+         * caller must match ALL requirements to access the schema. For example, a caller must hold
+         * required permissions AND it is a specified package.
+         *
+         * <p> The querier could have access if they match ALL requirements in ANY of the given
+         * {@link SchemaVisibilityConfig}s
+         *
+         * <p>For example, if the Set contains {@code {% verbatim %}{{PackageA and Permission1},
+         * {PackageB and Permission2}}{% endverbatim %}}.
+         * <ul>
+         *     <li>A querier from packageA could read if they holds Permission1.</li>
+         *     <li>A querier from packageA could NOT read if they only holds Permission2 instead of
+         *     Permission1.</li>
+         *     <li>A querier from packageB could read if they holds Permission2.</li>
+         *     <li>A querier from packageC could never read.</li>
+         *     <li>A querier holds both PermissionA and PermissionE has access.</li>
+         * </ul>
+         *
+         * @param schemaType         The schema type to set visibility on.
+         * @param visibleToConfigs   The {@link SchemaVisibilityConfig}s hold all requirements that
+         *                           a call must to match to access the schema.
+         */
+        // Merged map available from getSchemasVisibleToConfigs
+        @CanIgnoreReturnValue
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+        @NonNull
+        public Builder setSchemaTypeVisibleToConfigs(@NonNull String schemaType,
+                @NonNull Set<SchemaVisibilityConfig> visibleToConfigs) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(visibleToConfigs);
+            resetIfBuilt();
+            InternalVisibilityConfig.Builder visibilityConfigBuilder =
+                    getOrCreateVisibilityConfigBuilder(schemaType);
+            for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
+                visibilityConfigBuilder.addVisibleToConfig(visibleToConfig);
+            }
             return this;
         }
 
@@ -416,17 +565,14 @@
          * @exportToFramework:hide
          */
          // Visibility setting is determined by SDK version, so it won't be needed in framework
+        @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
         public Builder setVisibilitySettingSupported(boolean visibilitySettingSupported) {
             if (visibilitySettingSupported) {
-                mSchemasNotDisplayedBySystem = new ArrayList<>();
-                mSchemasVisibleToPackages = new Bundle();
-                mSchemasVisibleToPermissions = new Bundle();
+                mVisibilityConfigBuilders = new ArrayMap<>();
             } else {
-                mSchemasNotDisplayedBySystem = null;
-                mSchemasVisibleToPackages = null;
-                mSchemasVisibleToPermissions = null;
+                mVisibilityConfigBuilders = null;
             }
             return this;
         }
@@ -434,33 +580,37 @@
         /** 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);
+            List<InternalVisibilityConfig> visibilityConfigs = null;
+            if (mVisibilityConfigBuilders != null) {
+                visibilityConfigs = new ArrayList<>();
+                for (InternalVisibilityConfig.Builder builder :
+                        mVisibilityConfigBuilders.values()) {
+                    visibilityConfigs.add(builder.build());
+                }
             }
             mBuilt = true;
-            return new GetSchemaResponse(bundle);
+            return new GetSchemaResponse(mVersion, mSchemas, visibilityConfigs);
+        }
+
+        @NonNull
+        private InternalVisibilityConfig.Builder getOrCreateVisibilityConfigBuilder(
+                @NonNull String schemaType) {
+            if (mVisibilityConfigBuilders == null) {
+                throw new IllegalStateException("GetSchemaResponse is not configured with"
+                        + "visibility setting support");
+            }
+            InternalVisibilityConfig.Builder builder = mVisibilityConfigBuilders.get(schemaType);
+            if (builder == null) {
+                builder = new InternalVisibilityConfig.Builder(schemaType);
+                mVisibilityConfigBuilders.put(schemaType, builder);
+            }
+            return builder;
         }
 
         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;
-                }
+                // No need to copy mVisibilityConfigBuilders -- it gets copied during build().
+                mSchemas = new ArrayList<>(mSchemas);
                 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 7f9dfb0..17fbfbd 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -53,11 +53,9 @@
      * @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,
@@ -127,11 +125,9 @@
     // 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);
 
@@ -170,11 +166,9 @@
      * @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_REGISTER_OBSERVER_CALLBACK)
-    // @exportToFramework:endStrip()
     void registerObserverCallback(
             @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
@@ -204,11 +198,9 @@
      * @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_REGISTER_OBSERVER_CALLBACK)
-    // @exportToFramework:endStrip()
     void unregisterObserverCallback(
             @NonNull String targetPackageName, @NonNull ObserverCallback observer)
             throws AppSearchException;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
index 3a1da37..2ece210 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
@@ -16,11 +16,17 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.InternalSetSchemaResponseCreator;
 import androidx.core.util.Preconditions;
 
 /**
@@ -34,36 +40,30 @@
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class InternalSetSchemaResponse {
-
-    private static final String IS_SUCCESS_FIELD = "isSuccess";
-    private static final String SET_SCHEMA_RESPONSE_BUNDLE_FIELD = "setSchemaResponseBundle";
-    private static final String ERROR_MESSAGE_FIELD = "errorMessage";
-
-    private final Bundle mBundle;
-
-    public InternalSetSchemaResponse(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
-
-    private InternalSetSchemaResponse(boolean isSuccess,
-            @NonNull SetSchemaResponse setSchemaResponse,
-            @Nullable String errorMessage) {
-        Preconditions.checkNotNull(setSchemaResponse);
-        mBundle = new Bundle();
-        mBundle.putBoolean(IS_SUCCESS_FIELD, isSuccess);
-        mBundle.putBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD, setSchemaResponse.getBundle());
-        mBundle.putString(ERROR_MESSAGE_FIELD, errorMessage);
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
-     */
-    @NonNull
[email protected](creator = "InternalSetSchemaResponseCreator")
+public class InternalSetSchemaResponse extends AbstractSafeParcelable {
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public Bundle getBundle() {
-        return mBundle;
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<InternalSetSchemaResponse> CREATOR =
+            new InternalSetSchemaResponseCreator();
+
+    @Field(id = 1, getter = "isSuccess")
+    private final boolean mIsSuccess;
+
+    @Field(id = 2, getter = "getSetSchemaResponse")
+    private final SetSchemaResponse mSetSchemaResponse;
+    @Field(id = 3, getter = "getErrorMessage")
+    @Nullable private final String mErrorMessage;
+
+    @Constructor
+    public InternalSetSchemaResponse(
+            @Param(id = 1) boolean isSuccess,
+            @Param(id = 2) @NonNull SetSchemaResponse setSchemaResponse,
+            @Param(id = 3) @Nullable String errorMessage) {
+        Preconditions.checkNotNull(setSchemaResponse);
+        mIsSuccess = isSuccess;
+        mSetSchemaResponse = setSchemaResponse;
+        mErrorMessage = errorMessage;
     }
 
     /**
@@ -94,7 +94,7 @@
 
     /** Returns {@code true} if the schema request is proceeded successfully. */
     public boolean isSuccess() {
-        return mBundle.getBoolean(IS_SUCCESS_FIELD);
+        return mIsSuccess;
     }
 
     /**
@@ -104,7 +104,7 @@
      */
     @NonNull
     public SetSchemaResponse getSetSchemaResponse() {
-        return new SetSchemaResponse(mBundle.getBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD));
+        return mSetSchemaResponse;
     }
 
 
@@ -115,6 +115,13 @@
      */
     @Nullable
     public String getErrorMessage() {
-        return mBundle.getString(ERROR_MESSAGE_FIELD);
+        return mErrorMessage;
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        InternalSetSchemaResponseCreator.writeToParcel(this, dest, flags);
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
new file mode 100644
index 0000000..843d1cf
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.InternalVisibilityConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An expanded version of {@link SchemaVisibilityConfig} which includes fields for internal use by
+ * AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "InternalVisibilityConfigCreator")
+public final class InternalVisibilityConfig extends AbstractSafeParcelable {
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final Parcelable.Creator<InternalVisibilityConfig> CREATOR =
+            new InternalVisibilityConfigCreator();
+
+    /**
+     * Build the List of {@link InternalVisibilityConfig}s from visibility settings.
+     */
+    @NonNull
+    public static List<InternalVisibilityConfig> toInternalVisibilityConfigs(
+            @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();
+        Map<String, PackageIdentifier> publiclyVisibleSchemas =
+                setSchemaRequest.getPubliclyVisibleSchemas();
+        Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs =
+                setSchemaRequest.getSchemasVisibleToConfigs();
+
+        List<InternalVisibilityConfig> result = new ArrayList<>(searchSchemas.size());
+        for (AppSearchSchema searchSchema : searchSchemas) {
+            String schemaType = searchSchema.getSchemaType();
+            InternalVisibilityConfig.Builder builder =
+                    new InternalVisibilityConfig.Builder(schemaType)
+                            .setNotDisplayedBySystem(
+                                    schemasNotDisplayedBySystem.contains(schemaType));
+
+            Set<PackageIdentifier> visibleToPackages = schemasVisibleToPackages.get(schemaType);
+            if (visibleToPackages != null) {
+                for (PackageIdentifier packageIdentifier : visibleToPackages) {
+                    builder.addVisibleToPackage(packageIdentifier);
+                }
+            }
+
+            Set<Set<Integer>> visibleToPermissionSets = schemasVisibleToPermissions.get(schemaType);
+            if (visibleToPermissionSets != null) {
+                for (Set<Integer> visibleToPermissions : visibleToPermissionSets) {
+                    builder.addVisibleToPermissions(visibleToPermissions);
+                }
+            }
+
+            PackageIdentifier publiclyVisibleTargetPackage = publiclyVisibleSchemas.get(schemaType);
+            if (publiclyVisibleTargetPackage != null) {
+                builder.setPubliclyVisibleTargetPackage(publiclyVisibleTargetPackage);
+            }
+
+            Set<SchemaVisibilityConfig> visibleToConfigs = schemasVisibleToConfigs.get(schemaType);
+            if (visibleToConfigs != null) {
+                for (SchemaVisibilityConfig schemaVisibilityConfig : visibleToConfigs) {
+                    builder.addVisibleToConfig(schemaVisibilityConfig);
+                }
+            }
+
+            result.add(builder.build());
+        }
+        return result;
+    }
+
+    @NonNull
+    @Field(id = 1, getter = "getSchemaType")
+    private final String mSchemaType;
+
+    @Field(id = 2, getter = "isNotDisplayedBySystem")
+    private final boolean mIsNotDisplayedBySystem;
+
+    /** The public visibility settings available in VisibilityConfig. */
+    @NonNull
+    @Field(id = 3, getter = "getVisibilityConfig")
+    private final SchemaVisibilityConfig mVisibilityConfig;
+
+    /** Extended visibility settings from {@link SetSchemaRequest#getSchemasVisibleToConfigs()} */
+    @NonNull
+    @Field(id = 4)
+    final List<SchemaVisibilityConfig> mVisibleToConfigs;
+
+    @Constructor
+    InternalVisibilityConfig(
+            @Param(id = 1) @NonNull String schemaType,
+            @Param(id = 2) boolean isNotDisplayedBySystem,
+            @Param(id = 3) @NonNull SchemaVisibilityConfig schemaVisibilityConfig,
+            @Param(id = 4) @NonNull List<SchemaVisibilityConfig> visibleToConfigs) {
+        mIsNotDisplayedBySystem = isNotDisplayedBySystem;
+        mSchemaType = Objects.requireNonNull(schemaType);
+        mVisibilityConfig = Objects.requireNonNull(schemaVisibilityConfig);
+        mVisibleToConfigs = Objects.requireNonNull(visibleToConfigs);
+    }
+
+    /**
+     * Gets the schemaType for this VisibilityConfig.
+     *
+     * <p>This is being used as the document id when we convert a {@link InternalVisibilityConfig}
+     * to a {@link GenericDocument}.
+     */
+    @NonNull
+    public String getSchemaType() {
+        return mSchemaType;
+    }
+
+    /** Returns whether this schema is visible to the system. */
+    public boolean isNotDisplayedBySystem() {
+        return mIsNotDisplayedBySystem;
+    }
+
+    /**
+     * Returns the visibility settings stored in the public {@link SchemaVisibilityConfig} object.
+     */
+    @NonNull
+    public SchemaVisibilityConfig getVisibilityConfig() {
+        return mVisibilityConfig;
+    }
+
+    /**
+     * Returns required {@link SchemaVisibilityConfig} sets for a caller need to match to access the
+     * schema this {@link InternalVisibilityConfig} represents.
+     */
+    @NonNull
+    public Set<SchemaVisibilityConfig> getVisibleToConfigs() {
+        return new ArraySet<>(mVisibleToConfigs);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        InternalVisibilityConfigCreator.writeToParcel(this, dest, flags);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof InternalVisibilityConfig)) {
+            return false;
+        }
+        InternalVisibilityConfig that = (InternalVisibilityConfig) o;
+        return mIsNotDisplayedBySystem == that.mIsNotDisplayedBySystem
+                && Objects.equals(mSchemaType, that.mSchemaType)
+                && Objects.equals(mVisibilityConfig, that.mVisibilityConfig)
+                && Objects.equals(mVisibleToConfigs, that.mVisibleToConfigs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsNotDisplayedBySystem, mSchemaType, mVisibilityConfig,
+                mVisibleToConfigs);
+    }
+
+    /** The builder class of {@link InternalVisibilityConfig}. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+    public static final class Builder {
+        private String mSchemaType;
+        private boolean mIsNotDisplayedBySystem;
+        private SchemaVisibilityConfig.Builder mVisibilityConfigBuilder;
+        private List<SchemaVisibilityConfig> mVisibleToConfigs = new ArrayList<>();
+        private boolean mBuilt;
+
+        /**
+         * Creates a {@link Builder} for a {@link InternalVisibilityConfig}.
+         *
+         * @param schemaType The SchemaType of the {@link AppSearchSchema} that this {@link
+         *                   InternalVisibilityConfig} 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 InternalVisibilityConfig}. This will be
+         *                   used as as an AppSearch id.
+         * @see GenericDocument#getId
+         */
+        public Builder(@NonNull String schemaType) {
+            mSchemaType = Objects.requireNonNull(schemaType);
+            mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder();
+        }
+
+        /** Creates a {@link Builder} from an existing {@link InternalVisibilityConfig} */
+        public Builder(@NonNull InternalVisibilityConfig internalVisibilityConfig) {
+            Objects.requireNonNull(internalVisibilityConfig);
+            mSchemaType = internalVisibilityConfig.mSchemaType;
+            mIsNotDisplayedBySystem = internalVisibilityConfig.mIsNotDisplayedBySystem;
+            mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(
+                    internalVisibilityConfig.getVisibilityConfig());
+            mVisibleToConfigs = internalVisibilityConfig.mVisibleToConfigs;
+        }
+
+        /** Sets schemaType, which will be as the id when converting to {@link GenericDocument}. */
+        @NonNull
+        @CanIgnoreReturnValue
+        public Builder setSchemaType(@NonNull String schemaType) {
+            resetIfBuilt();
+            mSchemaType = Objects.requireNonNull(schemaType);
+            return this;
+        }
+
+        /**
+         * Resets all values contained in the VisibilityConfig with the values from the given
+         * VisibiltiyConfig.
+         */
+        @NonNull
+        @CanIgnoreReturnValue
+        public Builder setVisibilityConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+            resetIfBuilt();
+            mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(schemaVisibilityConfig);
+            return this;
+        }
+
+        /**
+         * Sets whether this schema has opted out of platform surfacing.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
+            resetIfBuilt();
+            mIsNotDisplayedBySystem = notDisplayedBySystem;
+            return this;
+        }
+
+        /**
+         * Add {@link PackageIdentifier} of packages which has access to this schema.
+         *
+         * @see SchemaVisibilityConfig.Builder#addAllowedPackage
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
+            resetIfBuilt();
+            mVisibilityConfigBuilder.addAllowedPackage(packageIdentifier);
+            return this;
+        }
+
+        /**
+         * Clears the list of packages which have access to this schema.
+         *
+         * @see SchemaVisibilityConfig.Builder#clearAllowedPackages
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearVisibleToPackages() {
+            resetIfBuilt();
+            mVisibilityConfigBuilder.clearAllowedPackages();
+            return this;
+        }
+
+        /**
+         * Adds a set of required Android {@link android.Manifest.permission} combination a package
+         * needs to hold to access the schema.
+         *
+         * @see SchemaVisibilityConfig.Builder#addRequiredPermissions
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addVisibleToPermissions(@NonNull Set<Integer> visibleToPermissions) {
+            resetIfBuilt();
+            mVisibilityConfigBuilder.addRequiredPermissions(visibleToPermissions);
+            return this;
+        }
+
+        /**
+         * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.
+         *
+         * @see SchemaVisibilityConfig.Builder#clearRequiredPermissions
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearVisibleToPermissions() {
+            resetIfBuilt();
+            mVisibilityConfigBuilder.clearRequiredPermissions();
+            return this;
+        }
+
+        /**
+         * Specify that this schema should be publicly available, to the same packages that have
+         * visibility to the package passed as a parameter. This visibility is determined by the
+         * result of {@link android.content.pm.PackageManager#canPackageQuery}.
+         *
+         * @see SchemaVisibilityConfig.Builder#setPubliclyVisibleTargetPackage
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setPubliclyVisibleTargetPackage(
+                @Nullable PackageIdentifier packageIdentifier) {
+            resetIfBuilt();
+            mVisibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier);
+            return this;
+        }
+
+        /**
+         * Add the {@link SchemaVisibilityConfig} for a caller need to match to access the schema
+         * this {@link InternalVisibilityConfig} represents.
+         *
+         * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig},
+         * and the querier will have access if they match ANY of the
+         * {@link SchemaVisibilityConfig}.
+         *
+         * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} hold all requirements
+         *                               that a call must match to access the schema.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addVisibleToConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+            Objects.requireNonNull(schemaVisibilityConfig);
+            resetIfBuilt();
+            mVisibleToConfigs.add(schemaVisibilityConfig);
+            return this;
+        }
+
+        /** Clears the set of {@link SchemaVisibilityConfig} which have access to this schema. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearVisibleToConfig() {
+            resetIfBuilt();
+            mVisibleToConfigs.clear();
+            return this;
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mVisibleToConfigs = new ArrayList<>(mVisibleToConfigs);
+                mBuilt = false;
+            }
+        }
+
+        /** Build a {@link InternalVisibilityConfig} */
+        @NonNull
+        public InternalVisibilityConfig build() {
+            mBuilt = true;
+            return new InternalVisibilityConfig(
+                    mSchemaType,
+                    mIsNotDisplayedBySystem,
+                    mVisibilityConfigBuilder.build(),
+                    mVisibleToConfigs);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
new file mode 100644
index 0000000..bd13255
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.File;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Contains utility methods for Framework implementation of AppSearch.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JetpackAppSearchEnvironment implements AppSearchEnvironment {
+
+    /**
+     * Returns AppSearch directory in the credential encrypted system directory for the given user.
+     *
+     * <p>This folder should only be accessed after unlock.
+     */
+    @Override
+    @NonNull
+    public File getAppSearchDir(@NonNull Context context, @Nullable UserHandle unused) {
+        return new File(context.getFilesDir(), "appsearch");
+    }
+
+    /** Creates context for the user based on the userHandle. */
+    @Override
+    @NonNull
+    public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) {
+        return context;
+    }
+
+    /** Creates and returns a ThreadPoolExecutor for given parameters. */
+    @Override
+    @NonNull
+    public ExecutorService createExecutorService(
+            int corePoolSize,
+            int maxConcurrency,
+            long keepAliveTime,
+            @NonNull TimeUnit unit,
+            @NonNull BlockingQueue<Runnable> workQueue,
+            int priority) {
+        return new ThreadPoolExecutor(
+                corePoolSize,
+                maxConcurrency,
+                keepAliveTime,
+                unit,
+                workQueue);
+    }
+
+    /** Creates and returns an ExecutorService with a single thread. */
+    @Override
+    @NonNull
+    public ExecutorService createSingleThreadExecutor() {
+        return Executors.newSingleThreadExecutor();
+    }
+
+    /** Creates and returns an Executor with cached thread pools. */
+    @Override
+    @NonNull
+    public ExecutorService createCachedThreadPoolExecutor() {
+        return Executors.newCachedThreadPool();
+    }
+
+    /**
+     * Returns a cache directory for creating temporary files like in case of migrating documents.
+     */
+    @Override
+    @Nullable
+    public File getCacheDir(@NonNull Context context) {
+        return context.getCacheDir();
+    }
+
+    @Override
+    public boolean isInfoLoggingEnabled() {
+        // INFO logging is enabled by default in Jetpack AppSearch.
+        return true;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
index 3639a79..16aff14 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
@@ -16,16 +16,23 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.JoinSpecCreator;
 import androidx.core.util.Preconditions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
 /**
  * This class represents the specifications for the joining operation in search.
@@ -117,12 +124,29 @@
  * return the signals calculated by scoring the joined documents using the scoring strategy in the
  * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}.
  */
-public final class JoinSpec {
-    static final String NESTED_QUERY = "nestedQuery";
-    static final String NESTED_SEARCH_SPEC = "nestedSearchSpec";
-    static final String CHILD_PROPERTY_EXPRESSION = "childPropertyExpression";
-    static final String MAX_JOINED_RESULT_COUNT = "maxJoinedResultCount";
-    static final String AGGREGATION_SCORING_STRATEGY = "aggregationScoringStrategy";
[email protected](creator = "JoinSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class JoinSpec extends AbstractSafeParcelable {
+    /** Creator class for {@link JoinSpec}. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<JoinSpec> CREATOR = new JoinSpecCreator();
+
+    @Field(id = 1, getter = "getNestedQuery")
+    private final String mNestedQuery;
+
+    @Field(id = 2, getter = "getNestedSearchSpec")
+    private final SearchSpec mNestedSearchSpec;
+
+    @Field(id = 3, getter = "getChildPropertyExpression")
+    private final String mChildPropertyExpression;
+
+    @Field(id = 4, getter = "getMaxJoinedResultCount")
+    private final int mMaxJoinedResultCount;
+
+    @Field(id = 5, getter = "getAggregationScoringStrategy")
+    private final int mAggregationScoringStrategy;
 
     private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10;
 
@@ -158,8 +182,10 @@
     public @interface AggregationScoringStrategy {
     }
 
-    /** Do not score the aggregation of joined documents. This is for the case where we want to
-     * perform a join, but keep the parent ranking signal. */
+    /**
+     * Do not score the aggregation of joined documents. This is for the case where we want to
+     * perform a join, but keep the parent ranking signal.
+     */
     public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0;
     /** Score the aggregation of joined documents by counting the number of results. */
     public static final int AGGREGATION_SCORING_RESULT_COUNT = 1;
@@ -172,33 +198,27 @@
     /** Score the aggregation of joined documents using the sum of ranking signal. */
     public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;
 
-    private final Bundle mBundle;
-
-    /** @exportToFramework:hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public JoinSpec(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
+    @Constructor
+    JoinSpec(
+            @Param(id = 1) @NonNull String nestedQuery,
+            @Param(id = 2) @NonNull SearchSpec nestedSearchSpec,
+            @Param(id = 3) @NonNull String childPropertyExpression,
+            @Param(id = 4) int maxJoinedResultCount,
+            @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy) {
+        mNestedQuery = Objects.requireNonNull(nestedQuery);
+        mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec);
+        mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression);
+        mMaxJoinedResultCount = maxJoinedResultCount;
+        mAggregationScoringStrategy = aggregationScoringStrategy;
     }
 
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     *
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
-    }
 
     /**
      * Returns the query to run on the joined documents.
-     *
      */
     @NonNull
     public String getNestedQuery() {
-        return mBundle.getString(NESTED_QUERY);
+        return mNestedQuery;
     }
 
     /**
@@ -211,7 +231,7 @@
      */
     @NonNull
     public String getChildPropertyExpression() {
-        return mBundle.getString(CHILD_PROPERTY_EXPRESSION);
+        return mChildPropertyExpression;
     }
 
     /**
@@ -219,7 +239,7 @@
      * with a default of 10 SearchResults.
      */
     public int getMaxJoinedResultCount() {
-        return mBundle.getInt(MAX_JOINED_RESULT_COUNT);
+        return mMaxJoinedResultCount;
     }
 
     /**
@@ -231,7 +251,7 @@
      */
     @NonNull
     public SearchSpec getNestedSearchSpec() {
-        return new SearchSpec(mBundle.getBundle(NESTED_SEARCH_SPEC));
+        return mNestedSearchSpec;
     }
 
     /**
@@ -244,7 +264,14 @@
      */
     @AggregationScoringStrategy
     public int getAggregationScoringStrategy() {
-        return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);
+        return mAggregationScoringStrategy;
+    }
+
+    @Override
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        JoinSpecCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link JoinSpec objects}. */
@@ -257,7 +284,8 @@
         private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
         private final String mChildPropertyExpression;
         private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
-        @AggregationScoringStrategy private int mAggregationScoringStrategy =
+        @AggregationScoringStrategy
+        private int mAggregationScoringStrategy =
                 AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
 
         /**
@@ -289,6 +317,17 @@
             mChildPropertyExpression = childPropertyExpression;
         }
 
+        /** @exportToFramework:hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull JoinSpec joinSpec) {
+            Preconditions.checkNotNull(joinSpec);
+            mNestedQuery = joinSpec.getNestedQuery();
+            mNestedSearchSpec = joinSpec.getNestedSearchSpec();
+            mChildPropertyExpression = joinSpec.getChildPropertyExpression();
+            mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount();
+            mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy();
+        }
+
         /**
          * Sets the query and the SearchSpec for the documents being joined. This will score and
          * rank the joined documents as well as filter the joined documents.
@@ -362,13 +401,13 @@
          */
         @NonNull
         public JoinSpec build() {
-            Bundle bundle = new Bundle();
-            bundle.putString(NESTED_QUERY, mNestedQuery);
-            bundle.putBundle(NESTED_SEARCH_SPEC, mNestedSearchSpec.getBundle());
-            bundle.putString(CHILD_PROPERTY_EXPRESSION, mChildPropertyExpression);
-            bundle.putInt(MAX_JOINED_RESULT_COUNT, mMaxJoinedResultCount);
-            bundle.putInt(AGGREGATION_SCORING_STRATEGY, mAggregationScoringStrategy);
-            return new JoinSpec(bundle);
+            return new JoinSpec(
+                    mNestedQuery,
+                    mNestedSearchSpec,
+                    mChildPropertyExpression,
+                    mMaxJoinedResultCount,
+                    mAggregationScoringStrategy
+            );
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
index 6c4bc59..c05d567 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -16,20 +16,18 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.safeparcel.PackageIdentifierParcel;
 import androidx.core.util.Preconditions;
 
-/** This class represents a uniquely identifiable package. */
+/**
+ * This class represents a uniquely identifiable package.
+ */
 public class PackageIdentifier {
-    private static final String PACKAGE_NAME_FIELD = "packageName";
-    private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
-
-    private final Bundle mBundle;
+    @NonNull
+    private final PackageIdentifierParcel mPackageIdentifierParcel;
 
     /**
      * Creates a unique identifier for a package.
@@ -43,36 +41,41 @@
      * new android.content.pm.Signature(outputDigest).toByteArray();
      * </pre>
      *
-     * @param packageName Name of the package.
+     * @param packageName       Name of the package.
      * @param sha256Certificate SHA-256 certificate digest of the package.
      */
     public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
-        mBundle = new Bundle();
-        mBundle.putString(PACKAGE_NAME_FIELD, packageName);
-        mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(sha256Certificate);
+        mPackageIdentifierParcel = new PackageIdentifierParcel(packageName, sha256Certificate);
     }
 
     /** @exportToFramework:hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public PackageIdentifier(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
+    public PackageIdentifier(@NonNull PackageIdentifierParcel packageIdentifierParcel) {
+        mPackageIdentifierParcel = Preconditions.checkNotNull(packageIdentifierParcel);
     }
 
-    /** @exportToFramework:hide */
+    /**
+     * Returns the {@link PackageIdentifierParcel} holding the values for this
+     * {@link PackageIdentifier}.
+     *
+     * @exportToFramework:hide
+     */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    public PackageIdentifierParcel getPackageIdentifierParcel() {
+        return mPackageIdentifierParcel;
     }
 
     @NonNull
     public String getPackageName() {
-        return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
+        return mPackageIdentifierParcel.getPackageName();
     }
 
     @NonNull
     public byte[] getSha256Certificate() {
-        return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
+        return mPackageIdentifierParcel.getSha256Certificate();
     }
 
     @Override
@@ -80,15 +83,15 @@
         if (this == obj) {
             return true;
         }
-        if (obj == null || !(obj instanceof PackageIdentifier)) {
+        if (!(obj instanceof PackageIdentifier)) {
             return false;
         }
         final PackageIdentifier other = (PackageIdentifier) obj;
-        return BundleUtil.deepEquals(mBundle, other.mBundle);
+        return mPackageIdentifierParcel.equals(other.getPackageIdentifierParcel());
     }
 
     @Override
     public int hashCode() {
-        return BundleUtil.deepHashCode(mBundle);
+        return mPackageIdentifierParcel.hashCode();
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
index e0557a6..0e69136 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
@@ -20,6 +20,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appsearch.checker.initialization.qual.UnderInitialization;
+import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
@@ -73,7 +75,9 @@
         }
     }
 
-    private void recursivePathScan(String path) throws IllegalArgumentException {
+    @RequiresNonNull("mPathList")
+    private void recursivePathScan(@UnderInitialization PropertyPath this, String path)
+            throws IllegalArgumentException {
         // Determine whether the path is just a raw property name with no control characters
         int controlPos = -1;
         boolean controlIsIndex = false;
@@ -128,7 +132,9 @@
      * @return the rest of the path after the end brackets, or null if there is nothing after them
      */
     @Nullable
-    private String consumePropertyWithIndex(@NonNull String path, int controlPos) {
+    @RequiresNonNull("mPathList")
+    private String consumePropertyWithIndex(
+            @UnderInitialization PropertyPath this, @NonNull String path, int controlPos) {
         Preconditions.checkNotNull(path);
         String propertyName = path.substring(0, controlPos);
         int endBracketIdx = path.indexOf(']', controlPos);
@@ -210,17 +216,23 @@
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null) return false;
-        if (!(o instanceof PropertyPath)) return false;
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof PropertyPath)) {
+            return false;
+        }
         PropertyPath that = (PropertyPath) o;
         return ObjectsCompat.equals(mPathList, that.mPathList);
     }
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(mPathList);
+        return ObjectsCompat.hashCode(mPathList);
     }
 
     /**
@@ -292,9 +304,7 @@
             mPropertyIndex = propertyIndex;
         }
 
-        /**
-         * @return the property name
-         */
+        /** Returns the name of the property. */
         @NonNull
         public String getPropertyName() {
             return mPropertyName;
@@ -319,10 +329,16 @@
         }
 
         @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null) return false;
-            if (!(o instanceof PathSegment)) return false;
+        public boolean equals(@Nullable Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null) {
+                return false;
+            }
+            if (!(o instanceof PathSegment)) {
+                return false;
+            }
             PathSegment that = (PathSegment) o;
             return mPropertyIndex == that.mPropertyIndex
                     && mPropertyName.equals(that.mPropertyName);
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 6edf85e..e0332b4 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -19,8 +19,13 @@
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.usagereporting.TakenAction;
+import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import java.util.ArrayList;
@@ -28,6 +33,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Encapsulates a request to index documents into an {@link AppSearchSession} database.
@@ -43,8 +49,11 @@
 public final class PutDocumentsRequest {
     private final List<GenericDocument> mDocuments;
 
-    PutDocumentsRequest(List<GenericDocument> documents) {
+    private final List<GenericDocument> mTakenActions;
+
+    PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions) {
         mDocuments = documents;
+        mTakenActions = takenActions;
     }
 
     /** Returns a list of {@link GenericDocument} objects that are part of this request. */
@@ -53,9 +62,27 @@
         return Collections.unmodifiableList(mDocuments);
     }
 
+    /**
+     * Returns a list of {@link GenericDocument} objects containing taken action metrics that are
+     * part of this request.
+     *
+     * <!--@exportToFramework:ifJetpack()-->
+     * <p>See {@link Builder#addTakenActions(TakenAction...)}.
+     * <!--@exportToFramework:else()
+     * <p>See {@link Builder#addTakenActionGenericDocuments(GenericDocument...)}.
+     * -->
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+    public List<GenericDocument> getTakenActionGenericDocuments() {
+        return Collections.unmodifiableList(mTakenActions);
+    }
+
+
     /** Builder for {@link PutDocumentsRequest} objects. */
     public static final class Builder {
         private ArrayList<GenericDocument> mDocuments = new ArrayList<>();
+        private ArrayList<GenericDocument> mTakenActions = new ArrayList<>();
         private boolean mBuilt = false;
 
         /** Adds one or more {@link GenericDocument} objects to the request. */
@@ -121,18 +148,219 @@
             }
             return addGenericDocuments(genericDocuments);
         }
+
+        /**
+         * Adds one or more {@link TakenAction} objects to the request.
+         *
+         * <p>Clients can construct {@link TakenAction} documents to report the user's actions on
+         * search results, and these actions can be used as signals to boost result ranking in
+         * future search requests. See {@link TakenAction} for more details.
+         *
+         * <p>Clients should report search and click actions together sorted by
+         * {@link TakenAction#getActionTimestampMillis} in chronological order.
+         * <p>For example, if there are 2 search actions, with 1 click action associated with the
+         * first and 2 click actions associated with the second, then clients should report
+         * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
+         *
+         * <p>Certain anonymized information about actions reported using this API may be uploaded
+         * using statsd and may be used to improve the quality of the search algorithms. Most of
+         * the information in this class is already non-identifiable, such as durations and its
+         * position in the result set. Identifiable information which you choose to provide, such
+         * as the query string, will be anonymized using techniques like Federated Analytics to
+         * ensure only the most frequently searched terms across the whole user population are
+         * retained and available for study.
+         *
+         * <p>You can alternatively use the {@link #addDocuments(Object...)} API with
+         * {@link TakenAction} document to retain the benefits of joining and using it on-device,
+         * without triggering any of the anonymized stats uploading described above.
+         *
+         * @param takenActions one or more {@link TakenAction} objects.
+         */
+        // Merged list available from getTakenActionGenericDocuments()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addTakenActions(
+                @NonNull TakenAction... takenActions) throws AppSearchException {
+            Preconditions.checkNotNull(takenActions);
+            resetIfBuilt();
+            return addTakenActions(Arrays.asList(takenActions));
+        }
+
+        /**
+         * Adds a collection of {@link TakenAction} objects to the request.
+         *
+         * @see #addTakenActions(TakenAction...)
+         *
+         * @param takenActions a collection of {@link TakenAction} objects.
+         */
+        // Merged list available from getTakenActionGenericDocuments()
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addTakenActions(@NonNull Collection<? extends TakenAction> takenActions)
+                throws AppSearchException {
+            Preconditions.checkNotNull(takenActions);
+            resetIfBuilt();
+            List<GenericDocument> genericDocuments = new ArrayList<>(takenActions.size());
+            for (Object takenAction : takenActions) {
+                GenericDocument genericDocument = GenericDocument.fromDocumentClass(takenAction);
+                genericDocuments.add(genericDocument);
+            }
+            mTakenActions.addAll(genericDocuments);
+            return this;
+        }
 // @exportToFramework:endStrip()
 
-        /** Creates a new {@link PutDocumentsRequest} object. */
+        /**
+         * Adds one or more {@link GenericDocument} objects containing taken action metrics to the
+         * request.
+         *
+         * <p>It is recommended to use taken action document classes in Jetpack library to construct
+         * taken action documents.
+         *
+         * <p>The document creation timestamp of the {@link GenericDocument} should be set to the
+         * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}.
+         *
+         * <p>Clients should report search and click actions together sorted by
+         * {@link GenericDocument#getCreationTimestampMillis} in chronological order.
+         * <p>For example, if there are 2 search actions, with 1 click action associated with the
+         * first and 2 click actions associated with the second, then clients should report
+         * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
+         *
+         * <p>Different types of taken actions and metrics to be collected by AppSearch:
+         * <ul>
+         *  <li>
+         *      Search action
+         *      <ul>
+         *          <li>actionType: LONG, the enum value of the action type.
+         *          <p>Requires to be {@code 1} for search actions.
+         *
+         *          <li>query: STRING, the user-entered search input (without any operators or
+         *          rewriting).
+         *
+         *          <li>fetchedResultCount: LONG, the number of {@link SearchResult} documents
+         *          fetched from AppSearch in this search action.
+         *      </ul>
+         *  </li>
+         *
+         *  <li>
+         *      Click action
+         *      <ul>
+         *          <li>actionType: LONG, the enum value of the action type.
+         *          <p>Requires to be {@code 2} for click actions.
+         *
+         *          <li>query: STRING, the user-entered search input (without any operators or
+         *          rewriting) that yielded the {@link SearchResult} on which the user took action.
+         *
+         *          <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
+         *          document that the user takes action on.
+         *          <p>A qualified id is a string generated by package, database, namespace, and
+         *          document id. See
+         *          {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId} for more
+         *          details.
+         *
+         *          <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among
+         *          the user-defined block.
+         *          <p>The client can define its own custom definition for block, for example,
+         *          corpus name, group, etc.
+         *          <p>For example, a client defines the block as corpus, and AppSearch returns 5
+         *          documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"].
+         *          Then the block ranks of them = [1, 2, 1, 1, 2].
+         *          <p>If the client is not presenting the results in multiple blocks, they should
+         *          set this value to match resultRankGlobal.
+         *
+         *          <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult}
+         *          document.
+         *          <p>Global rank reflects the order of {@link SearchResult} documents returned by
+         *          AppSearch.
+         *          <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult} documents
+         *          for each page. Then the global ranks of them will be 1 to 10 for the first page,
+         *          and 11 to 20 for the second page.
+         *
+         *          <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on
+         *          the {@link SearchResult} document after clicking it.
+         *      </ul>
+         *  </li>
+         * </ul>
+         *
+         * <p>Certain anonymized information about actions reported using this API may be uploaded
+         * using statsd and may be used to improve the quality of the search algorithms. Most of
+         * the information in this class is already non-identifiable, such as durations and its
+         * position in the result set. Identifiable information which you choose to provide, such
+         * as the query string, will be anonymized using techniques like Federated Analytics to
+         * ensure only the most frequently searched terms across the whole user population are
+         * retained and available for study.
+         *
+         * <p>You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to
+         * retain the benefits of joining and using it on-device, without triggering any of the
+         * anonymized stats uploading described above.
+         *
+         * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing
+         *                                    taken action metric fields.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+        public Builder addTakenActionGenericDocuments(
+                @NonNull GenericDocument... takenActionGenericDocuments) throws AppSearchException {
+            Preconditions.checkNotNull(takenActionGenericDocuments);
+            resetIfBuilt();
+            return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments));
+        }
+
+        /**
+         * Adds a collection of {@link GenericDocument} objects containing taken action metrics to
+         * the request.
+         *
+         * @see #addTakenActionGenericDocuments(GenericDocument...)
+         *
+         * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects
+         *                                    containing taken action metric fields.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
+        public Builder addTakenActionGenericDocuments(@NonNull Collection<?
+                extends GenericDocument> takenActionGenericDocuments) throws AppSearchException {
+            Preconditions.checkNotNull(takenActionGenericDocuments);
+            resetIfBuilt();
+            mTakenActions.addAll(takenActionGenericDocuments);
+            return this;
+        }
+
+        /**
+         * Creates a new {@link PutDocumentsRequest} object.
+         *
+         * @throws IllegalArgumentException if there is any id collision between normal and action
+         *                                  documents.
+         */
         @NonNull
         public PutDocumentsRequest build() {
             mBuilt = true;
-            return new PutDocumentsRequest(mDocuments);
+
+            // Verify there is no id collision between normal documents and action documents.
+            Set<String> idSet = new ArraySet<>();
+            for (int i = 0; i < mDocuments.size(); i++) {
+                idSet.add(mDocuments.get(i).getId());
+            }
+            for (int i = 0; i < mTakenActions.size(); i++) {
+                GenericDocument takenAction = mTakenActions.get(i);
+                if (idSet.contains(takenAction.getId())) {
+                    throw new IllegalArgumentException("Document id " + takenAction.getId()
+                            + " cannot exist in both taken action and normal document");
+                }
+            }
+
+            return new PutDocumentsRequest(mDocuments, mTakenActions);
         }
 
         private void resetIfBuilt() {
             if (mBuilt) {
                 mDocuments = new ArrayList<>(mDocuments);
+                mTakenActions = new ArrayList<>(mTakenActions);
                 mBuilt = false;
             }
         }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
index 40cd591..ce76455 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -16,14 +16,27 @@
 
 package androidx.appsearch.app;
 
+import android.os.Parcel;
+import android.os.Parcelable;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.RemoveByDocumentIdRequestCreator;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -32,13 +45,37 @@
  *
  * @see AppSearchSession#removeAsync
  */
-public final class RemoveByDocumentIdRequest {
-    private final String mNamespace;
-    private final Set<String> mIds;
[email protected](creator = "RemoveByDocumentIdRequestCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class RemoveByDocumentIdRequest extends AbstractSafeParcelable {
+    /** Creator class for {@link android.app.appsearch.RemoveByDocumentIdRequest}. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<RemoveByDocumentIdRequest> CREATOR =
+            new RemoveByDocumentIdRequestCreator();
 
-    RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
-        mNamespace = namespace;
-        mIds = ids;
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
+    private final String mNamespace;
+    @NonNull
+    @Field(id = 2)
+    final List<String> mIds;
+    @Nullable
+    private Set<String> mIdsCached;
+
+    /**
+     * Removes documents by ID.
+     *
+     * @param namespace    Namespace of the document to remove.
+     * @param ids The IDs of the documents to delete
+     */
+    @Constructor
+    RemoveByDocumentIdRequest(
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull List<String> ids) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mIds = Objects.requireNonNull(ids);
     }
 
     /** Returns the namespace to remove documents from. */
@@ -50,7 +87,17 @@
     /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getIds() {
-        return Collections.unmodifiableSet(mIds);
+        if (mIdsCached == null) {
+            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+        }
+        return mIdsCached;
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        RemoveByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link RemoveByDocumentIdRequest} objects. */
@@ -87,7 +134,7 @@
         @NonNull
         public RemoveByDocumentIdRequest build() {
             mBuilt = true;
-            return new RemoveByDocumentIdRequest(mNamespace, mIds);
+            return new RemoveByDocumentIdRequest(mNamespace, new ArrayList<>(mIds));
         }
 
         private void resetIfBuilt() {
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 58e7d9b..aafcc61 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -16,10 +16,21 @@
 
 package androidx.appsearch.app;
 
+import android.os.Parcel;
+import android.os.Parcelable;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.ReportUsageRequestCreator;
 import androidx.core.util.Preconditions;
 
+import java.util.Objects;
+
 /**
  * A request to report usage of a document.
  *
@@ -27,18 +38,34 @@
  *
  * @see AppSearchSession#reportUsageAsync
  */
-public final class ReportUsageRequest {
-    private final String mNamespace;
-    private final String mDocumentId;
-    private final long mUsageTimestampMillis;
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "ReportUsageRequestCreator")
+public final class ReportUsageRequest extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<ReportUsageRequest> CREATOR =
+            new ReportUsageRequestCreator();
 
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
+    private final String mNamespace;
+    @NonNull
+    @Field(id = 2, getter = "getDocumentId")
+    private final String mDocumentId;
+    @Field(id = 3, getter = "getUsageTimestampMillis")
+    private final  long mUsageTimestampMillis;
+
+    @Constructor
     ReportUsageRequest(
-            @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
-        mNamespace = Preconditions.checkNotNull(namespace);
-        mDocumentId = Preconditions.checkNotNull(documentId);
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull String documentId,
+            @Param(id = 3) long usageTimestampMillis) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mDocumentId = Objects.requireNonNull(documentId);
         mUsageTimestampMillis = usageTimestampMillis;
     }
 
+
     /** Returns the namespace of the document that was used. */
     @NonNull
     public String getNamespace() {
@@ -62,6 +89,13 @@
         return mUsageTimestampMillis;
     }
 
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ReportUsageRequestCreator.writeToParcel(this, dest, flags);
+    }
+
     /** Builder for {@link ReportUsageRequest} objects. */
     public static final class Builder {
         private final String mNamespace;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
new file mode 100644
index 0000000..32b69b3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.PackageIdentifierParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class to hold a all necessary Visibility information corresponding to the same schema. This
+ * pattern allows for easier association of these documents.
+ *
+ * <p> This does not correspond to any schema, the properties held in this class are kept in two
+ * separate schemas, VisibilityConfig and PublicAclOverlay.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
[email protected](creator = "VisibilityConfigCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SchemaVisibilityConfig extends AbstractSafeParcelable {
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final Parcelable.Creator<SchemaVisibilityConfig> CREATOR =
+            new VisibilityConfigCreator();
+
+    @NonNull
+    @Field(id = 1)
+    final List<PackageIdentifierParcel> mAllowedPackages;
+
+    @NonNull
+    @Field(id = 2)
+    final List<VisibilityPermissionConfig> mRequiredPermissions;
+
+    @Nullable
+    @Field(id = 3)
+    final PackageIdentifierParcel mPubliclyVisibleTargetPackage;
+
+    @Nullable private Integer mHashCode;
+    @Nullable private List<PackageIdentifier> mAllowedPackagesCached;
+    @Nullable private Set<Set<Integer>> mRequiredPermissionsCached;
+
+    @Constructor
+    SchemaVisibilityConfig(
+            @Param(id = 1) @NonNull List<PackageIdentifierParcel> allowedPackages,
+            @Param(id = 2) @NonNull List<VisibilityPermissionConfig> requiredPermissions,
+            @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage) {
+        mAllowedPackages = Objects.requireNonNull(allowedPackages);
+        mRequiredPermissions = Objects.requireNonNull(requiredPermissions);
+        mPubliclyVisibleTargetPackage = publiclyVisibleTargetPackage;
+    }
+
+     /** Returns a list of {@link PackageIdentifier}s of packages that can access this schema. */
+    @NonNull
+    public List<PackageIdentifier> getAllowedPackages() {
+        if (mAllowedPackagesCached == null) {
+            mAllowedPackagesCached = new ArrayList<>(mAllowedPackages.size());
+            for (int i = 0; i < mAllowedPackages.size(); i++) {
+                mAllowedPackagesCached.add(new PackageIdentifier(mAllowedPackages.get(i)));
+            }
+        }
+        return mAllowedPackagesCached;
+    }
+
+    /**
+     * Returns an array of Integers representing Android Permissions that the caller must hold to
+     * access the schema this {@link SchemaVisibilityConfig} represents.
+     * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
+     */
+    @NonNull
+    public Set<Set<Integer>> getRequiredPermissions() {
+        if (mRequiredPermissionsCached == null) {
+            mRequiredPermissionsCached = new ArraySet<>(mRequiredPermissions.size());
+            for (int i = 0; i < mRequiredPermissions.size(); i++) {
+                VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i);
+                Set<Integer> requiredPermissions = permissionConfig.getAllRequiredPermissions();
+                if (mRequiredPermissionsCached != null && requiredPermissions != null) {
+                    mRequiredPermissionsCached.add(requiredPermissions);
+                }
+            }
+        }
+        // Added for nullness checker as it is @Nullable, we initialize it above if it is null.
+        return Objects.requireNonNull(mRequiredPermissionsCached);
+    }
+
+    /**
+     * Returns the {@link PackageIdentifier} of the package that will be used as the target package
+     * in a call to {@link android.content.pm.PackageManager#canPackageQuery} to determine which
+     * packages can access this publicly visible schema. Returns null if the schema is not publicly
+     * visible.
+     */
+    @Nullable
+    public PackageIdentifier getPubliclyVisibleTargetPackage() {
+        if (mPubliclyVisibleTargetPackage == null) {
+            return null;
+        }
+        return new PackageIdentifier(mPubliclyVisibleTargetPackage);
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        VisibilityConfigCreator.writeToParcel(this, dest, flags);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof SchemaVisibilityConfig)) {
+            return false;
+        }
+        SchemaVisibilityConfig that = (SchemaVisibilityConfig) o;
+        return Objects.equals(mAllowedPackages, that.mAllowedPackages)
+                && Objects.equals(mRequiredPermissions, that.mRequiredPermissions)
+                && Objects.equals(
+                        mPubliclyVisibleTargetPackage, that.mPubliclyVisibleTargetPackage);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = Objects.hash(
+                    mAllowedPackages,
+                    mRequiredPermissions,
+                    mPubliclyVisibleTargetPackage);
+        }
+        return mHashCode;
+    }
+
+    /** The builder class of {@link SchemaVisibilityConfig}. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+    public static final class Builder {
+        private List<PackageIdentifierParcel> mAllowedPackages = new ArrayList<>();
+        private List<VisibilityPermissionConfig> mRequiredPermissions = new ArrayList<>();
+        @Nullable private PackageIdentifierParcel mPubliclyVisibleTargetPackage;
+        private boolean mBuilt;
+
+        /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */
+        public Builder() {}
+
+        /**
+         * Creates a {@link Builder} copying the values from an existing
+         * {@link SchemaVisibilityConfig}.
+         *
+         * @exportToFramework:hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+            Objects.requireNonNull(schemaVisibilityConfig);
+            mAllowedPackages = new ArrayList<>(schemaVisibilityConfig.mAllowedPackages);
+            mRequiredPermissions = new ArrayList<>(schemaVisibilityConfig.mRequiredPermissions);
+            mPubliclyVisibleTargetPackage = schemaVisibilityConfig.mPubliclyVisibleTargetPackage;
+        }
+
+        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addAllowedPackage(@NonNull PackageIdentifier packageIdentifier) {
+            Objects.requireNonNull(packageIdentifier);
+            resetIfBuilt();
+            mAllowedPackages.add(packageIdentifier.getPackageIdentifierParcel());
+            return this;
+        }
+
+        /** Clears the list of packages which have access to this schema. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearAllowedPackages() {
+            resetIfBuilt();
+            mAllowedPackages.clear();
+            return this;
+        }
+
+        /**
+         * Adds a set of required Android {@link android.Manifest.permission} combination a
+         * package needs to hold to access the schema this {@link SchemaVisibilityConfig}
+         * represents.
+         *
+         * <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 repeatedly to add multiple permission combinations, and the
+         * querier will have access if they holds ANY of the combinations.
+         *
+         * <p>Merged Set available from {@link #getRequiredPermissions()}.
+         *
+         * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility for
+         * supported Permissions.
+         */
+        @SuppressWarnings("RequiresPermission")  // No permission required to call this method
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addRequiredPermissions(@NonNull Set<Integer> visibleToPermissions) {
+            Objects.requireNonNull(visibleToPermissions);
+            resetIfBuilt();
+            mRequiredPermissions.add(new VisibilityPermissionConfig(visibleToPermissions));
+            return this;
+        }
+
+        /**
+         * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearRequiredPermissions() {
+            resetIfBuilt();
+            mRequiredPermissions.clear();
+            return this;
+        }
+
+        /**
+         * Specify that this schema should be publicly available, to the same packages that have
+         * visibility to the package passed as a parameter. This visibility is determined by the
+         * result of {@link android.content.pm.PackageManager#canPackageQuery}.
+         *
+         * <p> It is possible for the packageIdentifier parameter to be different from the
+         * package performing the indexing. This might happen in the case of an on-device indexer
+         * processing information about various packages. The visibility will be the same
+         * regardless of which package indexes the document, as the visibility is based on the
+         * packageIdentifier parameter.
+         *
+         * <p> Calling this with packageIdentifier set to null is valid, and will remove public
+         * visibility for the schema.
+         *
+         * @param packageIdentifier the {@link PackageIdentifier} of the package that will be used
+         *                          as the target package in a call to {@link
+         *                          android.content.pm.PackageManager#canPackageQuery} to determine
+         *                          which packages can access this publicly visible schema.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setPubliclyVisibleTargetPackage(
+                @Nullable PackageIdentifier packageIdentifier) {
+            resetIfBuilt();
+            if (packageIdentifier == null) {
+                mPubliclyVisibleTargetPackage = null;
+            } else {
+                mPubliclyVisibleTargetPackage = packageIdentifier.getPackageIdentifierParcel();
+            }
+            return this;
+        }
+
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mAllowedPackages = new ArrayList<>(mAllowedPackages);
+                mRequiredPermissions = new ArrayList<>(mRequiredPermissions);
+                mBuilt = false;
+            }
+        }
+
+        /** Build a {@link SchemaVisibilityConfig} */
+        @NonNull
+        public SchemaVisibilityConfig build() {
+            mBuilt = true;
+            return new SchemaVisibilityConfig(
+                    mAllowedPackages,
+                    mRequiredPermissions,
+                    mPubliclyVisibleTargetPackage);
+        }
+    }
+}
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 187304f..0fce482 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -16,7 +16,8 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -24,10 +25,18 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator;
 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;
 
@@ -46,36 +55,61 @@
  *
  * @see SearchResults
  */
-public final class SearchResult {
-    static final String DOCUMENT_FIELD = "document";
-    static final String MATCH_INFOS_FIELD = "matchInfos";
-    static final String PACKAGE_NAME_FIELD = "packageName";
-    static final String DATABASE_NAME_FIELD = "databaseName";
-    static final String RANKING_SIGNAL_FIELD = "rankingSignal";
-    static final String JOINED_RESULTS = "joinedResults";
[email protected](creator = "SearchResultCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchResult extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<SearchResult> CREATOR =
+            new SearchResultCreator();
 
+    @Field(id = 1)
+    final GenericDocumentParcel mDocument;
+    @Field(id = 2)
+    final List<MatchInfo> mMatchInfos;
+    @Field(id = 3, getter = "getPackageName")
+    private final String mPackageName;
+    @Field(id = 4, getter = "getDatabaseName")
+    private final String mDatabaseName;
+    @Field(id = 5, getter = "getRankingSignal")
+    private final double mRankingSignal;
+    @Field(id = 6, getter = "getJoinedResults")
+    private final List<SearchResult> mJoinedResults;
     @NonNull
-    private final Bundle mBundle;
+    @Field(id = 7, getter = "getInformationalRankingSignals")
+    private final List<Double> mInformationalRankingSignals;
 
-    /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */
-    @Nullable
-    private GenericDocument mDocument;
 
-    /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
+    /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
     @Nullable
-    private List<MatchInfo> mMatchInfos;
+    private GenericDocument mDocumentCached;
+
+    /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */
+    @Nullable
+    private List<MatchInfo> mMatchInfosCached;
 
     /** @exportToFramework:hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public SearchResult(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
-
-    /** @exportToFramework:hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    @Constructor
+    SearchResult(
+            @Param(id = 1) @NonNull GenericDocumentParcel document,
+            @Param(id = 2) @NonNull List<MatchInfo> matchInfos,
+            @Param(id = 3) @NonNull String packageName,
+            @Param(id = 4) @NonNull String databaseName,
+            @Param(id = 5) double rankingSignal,
+            @Param(id = 6) @NonNull List<SearchResult> joinedResults,
+            @Param(id = 7) @Nullable List<Double> informationalRankingSignals) {
+        mDocument = Preconditions.checkNotNull(document);
+        mMatchInfos = Preconditions.checkNotNull(matchInfos);
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mDatabaseName = Preconditions.checkNotNull(databaseName);
+        mRankingSignal = rankingSignal;
+        mJoinedResults = Collections.unmodifiableList(Preconditions.checkNotNull(joinedResults));
+        if (informationalRankingSignals != null) {
+            mInformationalRankingSignals = Collections.unmodifiableList(
+                    informationalRankingSignals);
+        } else {
+            mInformationalRankingSignals = Collections.emptyList();
+        }
     }
 
 // @exportToFramework:startStrip()
@@ -92,7 +126,7 @@
      * @see GenericDocument#toDocumentClass(Class)
      */
     @NonNull
-    public <T> T getDocument(@NonNull Class<T> documentClass) throws AppSearchException {
+    public <T> T getDocument(@NonNull java.lang.Class<T> documentClass) throws AppSearchException {
         return getDocument(documentClass, /* documentClassMap= */null);
     }
 
@@ -112,7 +146,7 @@
      * @see GenericDocument#toDocumentClass(Class, Map)
      */
     @NonNull
-    public <T> T getDocument(@NonNull Class<T> documentClass,
+    public <T> T getDocument(@NonNull java.lang.Class<T> documentClass,
             @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException {
         Preconditions.checkNotNull(documentClass);
         return getGenericDocument().toDocumentClass(documentClass, documentClassMap);
@@ -126,11 +160,10 @@
      */
     @NonNull
     public GenericDocument getGenericDocument() {
-        if (mDocument == null) {
-            mDocument = new GenericDocument(
-                    Preconditions.checkNotNull(mBundle.getBundle(DOCUMENT_FIELD)));
+        if (mDocumentCached == null) {
+            mDocumentCached = new GenericDocument(mDocument);
         }
-        return mDocument;
+        return mDocumentCached;
     }
 
     /**
@@ -143,22 +176,21 @@
      * value, this method returns an empty list.
      */
     @NonNull
-    @SuppressWarnings("deprecation")
     public List<MatchInfo> getMatchInfos() {
-        if (mMatchInfos == null) {
-            List<Bundle> matchBundles =
-                    Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
-            mMatchInfos = new ArrayList<>(matchBundles.size());
-            for (int i = 0; i < matchBundles.size(); i++) {
-                MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
-                if (mMatchInfos != null) {
+        if (mMatchInfosCached == null) {
+            mMatchInfosCached = new ArrayList<>(mMatchInfos.size());
+            for (int i = 0; i < mMatchInfos.size(); i++) {
+                MatchInfo matchInfo = mMatchInfos.get(i);
+                matchInfo.setDocument(getGenericDocument());
+                if (mMatchInfosCached != null) {
                     // This additional check is added for NullnessChecker.
-                    mMatchInfos.add(matchInfo);
+                    mMatchInfosCached.add(matchInfo);
                 }
             }
+            mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached);
         }
         // This check is added for NullnessChecker, mMatchInfos will always be NonNull.
-        return Preconditions.checkNotNull(mMatchInfos);
+        return Preconditions.checkNotNull(mMatchInfosCached);
     }
 
     /**
@@ -168,7 +200,7 @@
      */
     @NonNull
     public String getPackageName() {
-        return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
+        return mPackageName;
     }
 
     /**
@@ -178,7 +210,7 @@
      */
     @NonNull
     public String getDatabaseName() {
-        return Preconditions.checkNotNull(mBundle.getString(DATABASE_NAME_FIELD));
+        return mDatabaseName;
     }
 
     /**
@@ -207,7 +239,17 @@
      * @return Ranking signal of the document
      */
     public double getRankingSignal() {
-        return mBundle.getDouble(RANKING_SIGNAL_FIELD);
+        return mRankingSignal;
+    }
+
+    /**
+     * Returns the informational ranking signals of the {@link GenericDocument}, according to the
+     * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}.
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public List<Double> getInformationalRankingSignals() {
+        return mInformationalRankingSignals;
     }
 
     /**
@@ -225,28 +267,26 @@
      * @return a List of SearchResults containing joined documents.
      */
     @NonNull
-    @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated.
     public List<SearchResult> getJoinedResults() {
-        ArrayList<Bundle> bundles = mBundle.getParcelableArrayList(JOINED_RESULTS);
-        if (bundles == null) {
-            return new ArrayList<>();
-        }
-        List<SearchResult> res = new ArrayList<>(bundles.size());
-        for (int i = 0; i < bundles.size(); i++) {
-            res.add(new SearchResult(bundles.get(i)));
-        }
+        return mJoinedResults;
+    }
 
-        return res;
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SearchResultCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link SearchResult} objects. */
     public static final class Builder {
         private final String mPackageName;
         private final String mDatabaseName;
-        private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>();
+        private List<MatchInfo> mMatchInfos = new ArrayList<>();
         private GenericDocument mGenericDocument;
         private double mRankingSignal;
-        private ArrayList<Bundle> mJoinedResults = new ArrayList<>();
+        private List<Double> mInformationalRankingSignals = new ArrayList<>();
+        private List<SearchResult> mJoinedResults = new ArrayList<>();
         private boolean mBuilt = false;
 
         /**
@@ -260,6 +300,26 @@
             mDatabaseName = Preconditions.checkNotNull(databaseName);
         }
 
+        /** @exportToFramework:hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull SearchResult searchResult) {
+            Preconditions.checkNotNull(searchResult);
+            mPackageName = searchResult.getPackageName();
+            mDatabaseName = searchResult.getDatabaseName();
+            mGenericDocument = searchResult.getGenericDocument();
+            mRankingSignal = searchResult.getRankingSignal();
+            mInformationalRankingSignals = new ArrayList<>(
+                    searchResult.getInformationalRankingSignals());
+            List<MatchInfo> matchInfos = searchResult.getMatchInfos();
+            for (int i = 0; i < matchInfos.size(); i++) {
+                addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
+            }
+            List<SearchResult> joinedResults = searchResult.getJoinedResults();
+            for (int i = 0; i < joinedResults.size(); i++) {
+                addJoinedResult(joinedResults.get(i));
+            }
+        }
+
 // @exportToFramework:startStrip()
         /**
          * Sets the document which matched.
@@ -298,7 +358,7 @@
                     "This MatchInfo is already associated with a SearchResult and can't be "
                             + "reassigned");
             resetIfBuilt();
-            mMatchInfoBundles.add(matchInfo.mBundle);
+            mMatchInfos.add(matchInfo);
             return this;
         }
 
@@ -311,6 +371,17 @@
             return this;
         }
 
+        /** Adds the informational ranking signal of the matched document in this SearchResult. */
+        @CanIgnoreReturnValue
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        @NonNull
+        public Builder addInformationalRankingSignal(double rankingSignal) {
+            resetIfBuilt();
+            mInformationalRankingSignals.add(rankingSignal);
+            return this;
+        }
+
+
         /**
          * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
          * @param joinedResult The joined SearchResult to add.
@@ -319,28 +390,43 @@
         @NonNull
         public Builder addJoinedResult(@NonNull SearchResult joinedResult) {
             resetIfBuilt();
-            mJoinedResults.add(joinedResult.getBundle());
+            mJoinedResults.add(joinedResult);
+            return this;
+        }
+
+        /**
+         * Clears the {@link SearchResult}s that were joined.
+         *
+         * @exportToFramework:hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder clearJoinedResults() {
+            resetIfBuilt();
+            mJoinedResults.clear();
             return this;
         }
 
         /** Constructs a new {@link SearchResult}. */
         @NonNull
         public SearchResult build() {
-            Bundle bundle = new Bundle();
-            bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
-            bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
-            bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
-            bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
-            bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
-            bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults);
             mBuilt = true;
-            return new SearchResult(bundle);
+            return new SearchResult(
+                    mGenericDocument.getDocumentParcel(),
+                    mMatchInfos,
+                    mPackageName,
+                    mDatabaseName,
+                    mRankingSignal,
+                    mJoinedResults,
+                    mInformationalRankingSignals);
         }
 
         private void resetIfBuilt() {
             if (mBuilt) {
-                mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
+                mMatchInfos = new ArrayList<>(mMatchInfos);
                 mJoinedResults = new ArrayList<>(mJoinedResults);
+                mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
                 mBuilt = false;
             }
         }
@@ -410,20 +496,32 @@
      *      <li>{@link MatchInfo#getSnippet()} returns "Testing 1"</li>
      * </ul>
      */
-    public static final class MatchInfo {
-        /** The path of the matching snippet property. */
-        private static final String PROPERTY_PATH_FIELD = "propertyPath";
-        private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
-        private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
-        private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower";
-        private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper";
-        private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
-        private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
+    @SafeParcelable.Class(creator = "MatchInfoCreator")
+    @SuppressWarnings("HiddenSuperclass")
+    public static final class MatchInfo extends AbstractSafeParcelable {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+        @NonNull public static final Parcelable.Creator<MatchInfo> CREATOR =
+                new MatchInfoCreator();
 
+        /** The path of the matching snippet property. */
+        @Field(id = 1, getter = "getPropertyPath")
         private final String mPropertyPath;
+        @Field(id = 2)
+        final int mExactMatchRangeStart;
+        @Field(id = 3)
+        final int mExactMatchRangeEnd;
+        @Field(id = 4)
+        final int mSubmatchRangeStart;
+        @Field(id = 5)
+        final int mSubmatchRangeEnd;
+        @Field(id = 6)
+        final int mSnippetRangeStart;
+        @Field(id = 7)
+        final int mSnippetRangeEnd;
+
         @Nullable
         private PropertyPath mPropertyPathObject = null;
-        final Bundle mBundle;
 
         /**
          * Document which the match comes from.
@@ -432,7 +530,7 @@
          * {@link #getExactMatch}, will throw {@link NullPointerException}.
          */
         @Nullable
-        final GenericDocument mDocument;
+        private GenericDocument mDocument = null;
 
         /** Full text of the matched property. Populated on first use. */
         @Nullable
@@ -440,23 +538,35 @@
 
         /** Range of property that exactly matched the query. Populated on first use. */
         @Nullable
-        private MatchRange mExactMatchRange;
+        private MatchRange mExactMatchRangeCached;
 
         /**
          * Range of property that corresponds to the subsequence of the exact match that directly
          * matches a query term. Populated on first use.
          */
         @Nullable
-        private MatchRange mSubmatchRange;
+        private MatchRange mSubmatchRangeCached;
 
         /** Range of some reasonable amount of context around the query. Populated on first use. */
         @Nullable
-        private MatchRange mWindowRange;
+        private MatchRange mWindowRangeCached;
 
-        MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
-            mBundle = Preconditions.checkNotNull(bundle);
-            mDocument = document;
-            mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD));
+        @Constructor
+        MatchInfo(
+                @Param(id = 1) @NonNull String propertyPath,
+                @Param(id = 2) int exactMatchRangeStart,
+                @Param(id = 3) int exactMatchRangeEnd,
+                @Param(id = 4) int submatchRangeStart,
+                @Param(id = 5) int submatchRangeEnd,
+                @Param(id = 6) int snippetRangeStart,
+                @Param(id = 7) int snippetRangeEnd) {
+            mPropertyPath = Preconditions.checkNotNull(propertyPath);
+            mExactMatchRangeStart = exactMatchRangeStart;
+            mExactMatchRangeEnd = exactMatchRangeEnd;
+            mSubmatchRangeStart = submatchRangeStart;
+            mSubmatchRangeEnd = submatchRangeEnd;
+            mSnippetRangeStart = snippetRangeStart;
+            mSnippetRangeEnd = snippetRangeEnd;
         }
 
         /**
@@ -521,12 +631,12 @@
          */
         @NonNull
         public MatchRange getExactMatchRange() {
-            if (mExactMatchRange == null) {
-                mExactMatchRange = new MatchRange(
-                        mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
-                        mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
+            if (mExactMatchRangeCached == null) {
+                mExactMatchRangeCached = new MatchRange(
+                        mExactMatchRangeStart,
+                        mExactMatchRangeEnd);
             }
-            return mExactMatchRange;
+            return mExactMatchRangeCached;
         }
 
         /**
@@ -555,20 +665,18 @@
          * false.
          * <!--@exportToFramework:else()-->
          */
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
-        // @exportToFramework:endStrip()
         @NonNull
         public MatchRange getSubmatchRange() {
             checkSubmatchSupported();
-            if (mSubmatchRange == null) {
-                mSubmatchRange = new MatchRange(
-                        mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD),
-                        mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD));
+            if (mSubmatchRangeCached == null) {
+                mSubmatchRangeCached = new MatchRange(
+                        mSubmatchRangeStart,
+                        mSubmatchRangeEnd);
             }
-            return mSubmatchRange;
+            return mSubmatchRangeCached;
         }
 
         /**
@@ -585,11 +693,9 @@
          * false.
          * <!--@exportToFramework:else()-->
          */
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
-        // @exportToFramework:endStrip()
         @NonNull
         public CharSequence getSubmatch() {
             checkSubmatchSupported();
@@ -606,12 +712,12 @@
          */
         @NonNull
         public MatchRange getSnippetRange() {
-            if (mWindowRange == null) {
-                mWindowRange = new MatchRange(
-                        mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
-                        mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
+            if (mWindowRangeCached == null) {
+                mWindowRangeCached = new MatchRange(
+                        mSnippetRangeStart,
+                        mSnippetRangeEnd);
             }
-            return mWindowRange;
+            return mWindowRangeCached;
         }
 
         /**
@@ -634,7 +740,7 @@
         }
 
         private void checkSubmatchSupported() {
-            if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) {
+            if (mSubmatchRangeStart == -1) {
                 throw new UnsupportedOperationException(
                         "Submatch is not supported with this backend/Android API level "
                                 + "combination");
@@ -651,11 +757,29 @@
             return result;
         }
 
+        /**
+         * Sets the {@link GenericDocument} for {@link MatchInfo}.
+         *
+         * {@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument}
+         * This provides the ability to set {@link MatchInfo#mDocument}
+         */
+        void setDocument(@NonNull GenericDocument document) {
+            mDocument = document;
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            MatchInfoCreator.writeToParcel(this, dest, flags);
+        }
+
         /** Builder for {@link MatchInfo} objects. */
         public static final class Builder {
             private final String mPropertyPath;
             private MatchRange mExactMatchRange = new MatchRange(0, 0);
-            @Nullable private MatchRange mSubmatchRange;
+            int mSubmatchRangeStart = -1;
+            int mSubmatchRangeEnd = -1;
             private MatchRange mSnippetRange = new MatchRange(0, 0);
 
             /**
@@ -675,6 +799,17 @@
                 mPropertyPath = Preconditions.checkNotNull(propertyPath);
             }
 
+            /** @exportToFramework:hide */
+            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+            public Builder(@NonNull MatchInfo matchInfo) {
+                Preconditions.checkNotNull(matchInfo);
+                mPropertyPath = matchInfo.mPropertyPath;
+                mExactMatchRange = matchInfo.getExactMatchRange();
+                mSubmatchRangeStart = matchInfo.mSubmatchRangeStart;
+                mSubmatchRangeEnd = matchInfo.mSubmatchRangeEnd;
+                mSnippetRange = matchInfo.getSnippetRange();
+            }
+
             /** Sets the exact {@link MatchRange} corresponding to the given entry. */
             @CanIgnoreReturnValue
             @NonNull
@@ -684,11 +819,15 @@
             }
 
 
-            /** Sets the submatch {@link MatchRange} corresponding to the given entry. */
+            /**
+             * Sets the start and end of a submatch {@link MatchRange} corresponding
+             * to the given entry.
+             */
             @CanIgnoreReturnValue
             @NonNull
             public Builder setSubmatchRange(@NonNull MatchRange matchRange) {
-                mSubmatchRange = Preconditions.checkNotNull(matchRange);
+                mSubmatchRangeStart = matchRange.getStart();
+                mSubmatchRangeEnd = matchRange.getEnd();
                 return this;
             }
 
@@ -703,24 +842,14 @@
             /** Constructs a new {@link MatchInfo}. */
             @NonNull
             public MatchInfo build() {
-                Bundle bundle = new Bundle();
-                bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath);
-                bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart());
-                bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd());
-                if (mSubmatchRange != null) {
-                    // Only populate the submatch fields if it was actually set.
-                    bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart());
-                }
-
-                if (mSubmatchRange != null) {
-                    // Only populate the submatch fields if it was actually set.
-                    // Moved to separate block for Nullness Checker.
-                    bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd());
-                }
-
-                bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart());
-                bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd());
-                return new MatchInfo(bundle, /*document=*/ null);
+                return new MatchInfo(
+                    mPropertyPath,
+                    mExactMatchRange.getStart(),
+                    mExactMatchRange.getEnd(),
+                    mSubmatchRangeStart,
+                    mSubmatchRangeEnd,
+                    mSnippetRange.getStart(),
+                    mSnippetRange.getEnd());
             }
         }
     }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
index 568c2b6..d5d17a3 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
@@ -16,14 +16,16 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultPageCreator;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -32,26 +34,29 @@
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SearchResultPage {
-    public static final String RESULTS_FIELD = "results";
-    public static final String NEXT_PAGE_TOKEN_FIELD = "nextPageToken";
[email protected](creator = "SearchResultPageCreator")
+public class SearchResultPage extends AbstractSafeParcelable {
+    @NonNull public static final Parcelable.Creator<SearchResultPage> CREATOR =
+            new SearchResultPageCreator();
+
+    @Field(id = 1, getter = "getNextPageToken")
     private final long mNextPageToken;
-
     @Nullable
-    private List<SearchResult> mResults;
+    @Field(id = 2, getter = "getResults")
+    private final List<SearchResult> mResults;
 
-    @NonNull
-    private final Bundle mBundle;
-
-    public SearchResultPage(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-        mNextPageToken = mBundle.getLong(NEXT_PAGE_TOKEN_FIELD);
+    @Constructor
+    public SearchResultPage(
+            @Param(id = 1) long nextPageToken,
+            @Param(id = 2) @Nullable List<SearchResult> results) {
+        mNextPageToken = nextPageToken;
+        mResults = results;
     }
 
-    /** Returns the {@link Bundle} of this class. */
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    /** Default constructor for {@link SearchResultPage}. */
+    public SearchResultPage() {
+        mNextPageToken = 0;
+        mResults = Collections.emptyList();
     }
 
     /** Returns the Token to get next {@link SearchResultPage}. */
@@ -61,19 +66,15 @@
 
     /** Returns all {@link androidx.appsearch.app.SearchResult}s of this page */
     @NonNull
-    @SuppressWarnings("deprecation")
     public List<SearchResult> getResults() {
         if (mResults == null) {
-            ArrayList<Bundle> resultBundles = mBundle.getParcelableArrayList(RESULTS_FIELD);
-            if (resultBundles == null) {
-                mResults = Collections.emptyList();
-            } else {
-                mResults = new ArrayList<>(resultBundles.size());
-                for (int i = 0; i < resultBundles.size(); i++) {
-                    mResults.add(new SearchResult(resultBundles.get(i)));
-                }
-            }
+            return Collections.emptyList();
         }
         return mResults;
     }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SearchResultPageCreator.writeToParcel(this, dest, flags);
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 9b86fc3..1b43e15 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
@@ -28,6 +30,11 @@
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSpecCreator;
 import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
@@ -41,49 +48,119 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
  * This class represents the specification logic for AppSearch. It can be used to set the type of
  * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
  */
-public final class SearchSpec {
[email protected](creator = "SearchSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSpec extends AbstractSafeParcelable {
+
+    /**  Creator class for {@link SearchSpec}. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<SearchSpec> CREATOR =
+            new SearchSpecCreator();
+
     /**
      * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply
      * property paths to all results, excepting any types that have had their own, specific
      * property paths set.
+     *
+     * @deprecated use {@link #SCHEMA_TYPE_WILDCARD} instead.
      */
+    @Deprecated
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
     /**
      * Schema type to be used in {@link SearchSpec.Builder#addFilterProperties(String, Collection)}
-     * to apply property paths to all results, excepting any types that have had their own, specific
-     * property paths set.
-     * @exportToFramework:hide
+     * and {@link SearchSpec.Builder#addProjection} to apply property paths to all results,
+     * excepting any types that have had their own, specific property paths set.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
     public static final String SCHEMA_TYPE_WILDCARD = "*";
 
-    static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
-    static final String SCHEMA_FIELD = "schema";
-    static final String NAMESPACE_FIELD = "namespace";
-    static final String PROPERTY_FIELD = "property";
-    static final String PACKAGE_NAME_FIELD = "packageName";
-    static final String NUM_PER_PAGE_FIELD = "numPerPage";
-    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
-    static final String ORDER_FIELD = "order";
-    static final String SNIPPET_COUNT_FIELD = "snippetCount";
-    static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
-    static final String MAX_SNIPPET_FIELD = "maxSnippet";
-    static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
-    static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
-    static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
-    static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";
-    static final String JOIN_SPEC = "joinSpec";
-    static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression";
-    static final String ENABLED_FEATURES_FIELD = "enabledFeatures";
+    @Field(id = 1, getter = "getTermMatch")
+    private final int mTermMatchType;
 
-    /** @exportToFramework:hide */
+    @Field(id = 2, getter = "getFilterSchemas")
+    private final List<String> mSchemas;
+
+    @Field(id = 3, getter = "getFilterNamespaces")
+    private final List<String> mNamespaces;
+
+    @Field(id = 4)
+    final Bundle mTypePropertyFilters;
+
+    @Field(id = 5, getter = "getFilterPackageNames")
+    private final List<String> mPackageNames;
+
+    @Field(id = 6, getter = "getResultCountPerPage")
+    private final int mResultCountPerPage;
+
+    @Field(id = 7, getter = "getRankingStrategy")
+    @RankingStrategy
+    private final int mRankingStrategy;
+
+    @Field(id = 8, getter = "getOrder")
+    @Order
+    private final int mOrder;
+
+    @Field(id = 9, getter = "getSnippetCount")
+    private final int mSnippetCount;
+
+    @Field(id = 10, getter = "getSnippetCountPerProperty")
+    private final int mSnippetCountPerProperty;
+
+    @Field(id = 11, getter = "getMaxSnippetSize")
+    private final int mMaxSnippetSize;
+
+    @Field(id = 12)
+    final Bundle mProjectionTypePropertyMasks;
+
+    @Field(id = 13, getter = "getResultGroupingTypeFlags")
+    @GroupingType
+    private final int mResultGroupingTypeFlags;
+
+    @Field(id = 14, getter = "getResultGroupingLimit")
+    private final int mGroupingLimit;
+
+    @Field(id = 15)
+    final Bundle mTypePropertyWeightsField;
+
+    @Nullable
+    @Field(id = 16, getter = "getJoinSpec")
+    private final JoinSpec mJoinSpec;
+
+    @Field(id = 17, getter = "getAdvancedRankingExpression")
+    private final String mAdvancedRankingExpression;
+
+    @Field(id = 18, getter = "getEnabledFeatures")
+    private final List<String> mEnabledFeatures;
+
+    @Field(id = 19, getter = "getSearchSourceLogTag")
+    @Nullable private final String mSearchSourceLogTag;
+
+    @NonNull
+    @Field(id = 20, getter = "getSearchEmbeddings")
+    private final List<EmbeddingVector> mSearchEmbeddings;
+
+    @Field(id = 21, getter = "getDefaultEmbeddingSearchMetricType")
+    private final int mDefaultEmbeddingSearchMetricType;
+
+    @NonNull
+    @Field(id = 22, getter = "getInformationalRankingExpressions")
+    private final List<String> mInformationalRankingExpressions;
+
+    /**
+     * Default number of documents per page.
+     *
+     * @exportToFramework:hide
+     */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final int DEFAULT_NUM_PER_PAGE = 10;
 
@@ -224,43 +301,112 @@
     /**
      * Results should be grouped together by schema type for the purpose of enforcing a limit on the
      * number of results returned per schema type.
-     *
-     * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-     * @exportToFramework:hide TODO(b/291122592): Unhide in Mainline when API updates via
-     *   Mainline are possible.
-     * -->
      */
-    // @exportToFramework:startStrip()
     @RequiresFeature(
             enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
             name = Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)
-    // @exportToFramework:endStrip()
+    @FlaggedApi(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA)
     public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
 
-    private final Bundle mBundle;
-
-    /** @exportToFramework:hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public SearchSpec(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-    }
-
     /**
-     * Returns the {@link Bundle} populated by this builder.
+     * Type of scoring used to calculate similarity for embedding vectors. For details of each, see
+     * comments above each value.
      *
      * @exportToFramework:hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    // NOTE: The integer values of these constants must match the proto enum constants in
+    // {@link SearchSpecProto.EmbeddingQueryMetricType.Code}
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef(value = {
+            EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+            EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT,
+            EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EmbeddingSearchMetricType {
     }
 
+    /**
+     * Cosine similarity as metric for embedding search and ranking.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1;
+    /**
+     * Dot product similarity as metric for embedding search and ranking.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2;
+    /**
+     * Euclidean distance as metric for embedding search and ranking.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3;
+
+
+    @Constructor
+    SearchSpec(
+            @Param(id = 1) int termMatchType,
+            @Param(id = 2) @NonNull List<String> schemas,
+            @Param(id = 3) @NonNull List<String> namespaces,
+            @Param(id = 4) @NonNull Bundle properties,
+            @Param(id = 5) @NonNull List<String> packageNames,
+            @Param(id = 6) int resultCountPerPage,
+            @Param(id = 7) @RankingStrategy int rankingStrategy,
+            @Param(id = 8) @Order int order,
+            @Param(id = 9) int snippetCount,
+            @Param(id = 10) int snippetCountPerProperty,
+            @Param(id = 11) int maxSnippetSize,
+            @Param(id = 12) @NonNull Bundle projectionTypePropertyMasks,
+            @Param(id = 13) int resultGroupingTypeFlags,
+            @Param(id = 14) int groupingLimit,
+            @Param(id = 15) @NonNull Bundle typePropertyWeightsField,
+            @Param(id = 16) @Nullable JoinSpec joinSpec,
+            @Param(id = 17) @NonNull String advancedRankingExpression,
+            @Param(id = 18) @NonNull List<String> enabledFeatures,
+            @Param(id = 19) @Nullable String searchSourceLogTag,
+            @Param(id = 20) @Nullable List<EmbeddingVector> searchEmbeddings,
+            @Param(id = 21) int defaultEmbeddingSearchMetricType,
+            @Param(id = 22) @Nullable List<String> informationalRankingExpressions
+    ) {
+        mTermMatchType = termMatchType;
+        mSchemas = Collections.unmodifiableList(Preconditions.checkNotNull(schemas));
+        mNamespaces = Collections.unmodifiableList(Preconditions.checkNotNull(namespaces));
+        mTypePropertyFilters = Preconditions.checkNotNull(properties);
+        mPackageNames = Collections.unmodifiableList(Preconditions.checkNotNull(packageNames));
+        mResultCountPerPage = resultCountPerPage;
+        mRankingStrategy = rankingStrategy;
+        mOrder = order;
+        mSnippetCount = snippetCount;
+        mSnippetCountPerProperty = snippetCountPerProperty;
+        mMaxSnippetSize = maxSnippetSize;
+        mProjectionTypePropertyMasks = Preconditions.checkNotNull(projectionTypePropertyMasks);
+        mResultGroupingTypeFlags = resultGroupingTypeFlags;
+        mGroupingLimit = groupingLimit;
+        mTypePropertyWeightsField = Preconditions.checkNotNull(typePropertyWeightsField);
+        mJoinSpec = joinSpec;
+        mAdvancedRankingExpression = Preconditions.checkNotNull(advancedRankingExpression);
+        mEnabledFeatures = Collections.unmodifiableList(
+                Preconditions.checkNotNull(enabledFeatures));
+        mSearchSourceLogTag = searchSourceLogTag;
+        if (searchEmbeddings != null) {
+            mSearchEmbeddings = Collections.unmodifiableList(searchEmbeddings);
+        } else {
+            mSearchEmbeddings = Collections.emptyList();
+        }
+        mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+        if (informationalRankingExpressions != null) {
+            mInformationalRankingExpressions = Collections.unmodifiableList(
+                    informationalRankingExpressions);
+        } else {
+            mInformationalRankingExpressions = Collections.emptyList();
+        }
+    }
+
+
     /** Returns how the query terms should match terms in the index. */
     @TermMatch
     public int getTermMatch() {
-        return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
+        return mTermMatchType;
     }
 
     /**
@@ -270,11 +416,10 @@
      */
     @NonNull
     public List<String> getFilterSchemas() {
-        List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
-        if (schemas == null) {
+        if (mSchemas == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(schemas);
+        return mSchemas;
     }
 
     /**
@@ -284,19 +429,15 @@
      *
      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
      * by this function, rather than calling it multiple times.
-     *
-     * @exportToFramework:hide
      */
     @NonNull
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
     public Map<String, List<String>> getFilterProperties() {
-        Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
-                mBundle.getBundle(PROPERTY_FIELD));
-        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Set<String> schemas = mTypePropertyFilters.keySet();
         Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
         for (String schema : schemas) {
             typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
-                    typePropertyPathsBundle.getStringArrayList(schema)));
+                    mTypePropertyFilters.getStringArrayList(schema)));
         }
         return typePropertyPathsMap;
     }
@@ -308,11 +449,10 @@
      */
     @NonNull
     public List<String> getFilterNamespaces() {
-        List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
-        if (namespaces == null) {
+        if (mNamespaces == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(namespaces);
+        return mNamespaces;
     }
 
     /**
@@ -324,45 +464,44 @@
      */
     @NonNull
     public List<String> getFilterPackageNames() {
-        List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
-        if (packageNames == null) {
+        if (mPackageNames == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(packageNames);
+        return mPackageNames;
     }
 
     /** Returns the number of results per page in the result set. */
     public int getResultCountPerPage() {
-        return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
+        return mResultCountPerPage;
     }
 
     /** Returns the ranking strategy. */
     @RankingStrategy
     public int getRankingStrategy() {
-        return mBundle.getInt(RANKING_STRATEGY_FIELD);
+        return mRankingStrategy;
     }
 
     /** Returns the order of returned search results (descending or ascending). */
     @Order
     public int getOrder() {
-        return mBundle.getInt(ORDER_FIELD);
+        return mOrder;
     }
 
     /** Returns how many documents to generate snippets for. */
     public int getSnippetCount() {
-        return mBundle.getInt(SNIPPET_COUNT_FIELD);
+        return mSnippetCount;
     }
 
     /**
      * Returns how many matches for each property of a matching document to generate snippets for.
      */
     public int getSnippetCountPerProperty() {
-        return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD);
+        return mSnippetCountPerProperty;
     }
 
     /** Returns the maximum size of a snippet in characters. */
     public int getMaxSnippetSize() {
-        return mBundle.getInt(MAX_SNIPPET_FIELD);
+        return mMaxSnippetSize;
     }
 
     /**
@@ -377,13 +516,12 @@
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
-        Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
-                mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD));
-        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Set<String> schemas = mProjectionTypePropertyMasks.keySet();
         Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
         for (String schema : schemas) {
-            typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
-                    typePropertyPathsBundle.getStringArrayList(schema)));
+            typePropertyPathsMap.put(schema,
+                    Objects.requireNonNull(
+                            mProjectionTypePropertyMasks.getStringArrayList(schema)));
         }
         return typePropertyPathsMap;
     }
@@ -400,16 +538,19 @@
      */
     @NonNull
     public Map<String, List<PropertyPath>> getProjectionPaths() {
-        Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
-        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Set<String> schemas = mProjectionTypePropertyMasks.keySet();
         Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
         for (String schema : schemas) {
-            ArrayList<String> propertyPathList = typePropertyPathsBundle.getStringArrayList(schema);
-            List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
-            for (String p: propertyPathList) {
-                copy.add(new PropertyPath(p));
+            ArrayList<String> propertyPathList = mProjectionTypePropertyMasks.getStringArrayList(
+                    schema);
+            if (propertyPathList != null) {
+                List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
+                for (int i = 0; i < propertyPathList.size(); i++) {
+                    String p = propertyPathList.get(i);
+                    copy.add(new PropertyPath(p));
+                }
+                typePropertyPathsMap.put(schema, copy);
             }
-            typePropertyPathsMap.put(schema, copy);
         }
         return typePropertyPathsMap;
     }
@@ -425,18 +566,20 @@
      */
     @NonNull
     public Map<String, Map<String, Double>> getPropertyWeights() {
-        Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
-        Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+        Set<String> schemaTypes = mTypePropertyWeightsField.keySet();
         Map<String, Map<String, Double>> typePropertyWeightsMap = new ArrayMap<>(
                 schemaTypes.size());
         for (String schemaType : schemaTypes) {
-            Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
-            Set<String> propertyPaths = propertyPathBundle.keySet();
-            Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
-            for (String propertyPath : propertyPaths) {
-                propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));
+            Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
+            if (propertyPathBundle != null) {
+                Set<String> propertyPaths = propertyPathBundle.keySet();
+                Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+                for (String propertyPath : propertyPaths) {
+                    propertyPathWeights.put(propertyPath,
+                            propertyPathBundle.getDouble(propertyPath));
+                }
+                typePropertyWeightsMap.put(schemaType, propertyPathWeights);
             }
-            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
         }
         return typePropertyWeightsMap;
     }
@@ -452,19 +595,22 @@
      */
     @NonNull
     public Map<String, Map<PropertyPath, Double>> getPropertyWeightPaths() {
-        Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
-        Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+        Set<String> schemaTypes = mTypePropertyWeightsField.keySet();
         Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap = new ArrayMap<>(
                 schemaTypes.size());
         for (String schemaType : schemaTypes) {
-            Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
-            Set<String> propertyPaths = propertyPathBundle.keySet();
-            Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
-            for (String propertyPath : propertyPaths) {
-                propertyPathWeights.put(new PropertyPath(propertyPath),
-                        propertyPathBundle.getDouble(propertyPath));
+            Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
+            if (propertyPathBundle != null) {
+                Set<String> propertyPaths = propertyPathBundle.keySet();
+                Map<PropertyPath, Double> propertyPathWeights =
+                        new ArrayMap<>(propertyPaths.size());
+                for (String propertyPath : propertyPaths) {
+                    propertyPathWeights.put(
+                            new PropertyPath(propertyPath),
+                            propertyPathBundle.getDouble(propertyPath));
+                }
+                typePropertyWeightsMap.put(schemaType, propertyPathWeights);
             }
-            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
         }
         return typePropertyWeightsMap;
     }
@@ -475,7 +621,7 @@
      */
     @GroupingType
     public int getResultGroupingTypeFlags() {
-        return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
+        return mResultGroupingTypeFlags;
     }
 
     /**
@@ -485,7 +631,7 @@
      * {@link Builder#setResultGrouping(int, int)} was not called.
      */
     public int getResultGroupingLimit() {
-        return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
+        return mGroupingLimit;
     }
 
     /**
@@ -493,11 +639,7 @@
      */
     @Nullable
     public JoinSpec getJoinSpec() {
-        Bundle joinSpec = mBundle.getBundle(JOIN_SPEC);
-        if (joinSpec == null) {
-            return null;
-        }
-        return new JoinSpec(joinSpec);
+        return mJoinSpec;
     }
 
     /**
@@ -506,28 +648,104 @@
      */
     @NonNull
     public String getAdvancedRankingExpression() {
-        return mBundle.getString(ADVANCED_RANKING_EXPRESSION, "");
+        return mAdvancedRankingExpression;
+    }
+
+
+    /**
+     * Gets a tag to indicate the source of this search, or {@code null} if
+     * {@link Builder#setSearchSourceLogTag(String)} was not called.
+     *
+     * <p> Some AppSearch implementations may log a hash of this tag using statsd. This tag may be
+     * used for tracing performance issues and crashes to a component of an app.
+     *
+     * <p>Call {@link Builder#setSearchSourceLogTag} and give a unique value if you want to
+     * distinguish this search scenario with other search scenarios during performance analysis.
+     *
+     * <p>Under no circumstances will AppSearch log the raw String value using statsd, but it
+     * will be provided as-is to custom {@code AppSearchLogger} implementations you have
+     * registered in your app.
+     */
+    @Nullable
+    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+    public String getSearchSourceLogTag() {
+        return mSearchSourceLogTag;
+    }
+
+    /**
+     * Returns the list of {@link EmbeddingVector} for embedding search.
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public List<EmbeddingVector> getSearchEmbeddings() {
+        return mSearchEmbeddings;
+    }
+
+    /**
+     * Returns the default embedding metric type used for embedding search
+     * (see {@link AppSearchSession#search}) and ranking
+     * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).
+     */
+    @EmbeddingSearchMetricType
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public int getDefaultEmbeddingSearchMetricType() {
+        return mDefaultEmbeddingSearchMetricType;
+    }
+
+    /**
+     * Returns the informational ranking expressions.
+     *
+     * @see Builder#addInformationalRankingExpressions
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public List<String> getInformationalRankingExpressions() {
+        return mInformationalRankingExpressions;
     }
 
     /**
      * Returns whether the NUMERIC_SEARCH feature is enabled.
      */
     public boolean isNumericSearchEnabled() {
-        return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH);
+        return mEnabledFeatures.contains(FeatureConstants.NUMERIC_SEARCH);
     }
 
     /**
      * Returns whether the VERBATIM_SEARCH feature is enabled.
      */
     public boolean isVerbatimSearchEnabled() {
-        return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH);
+        return mEnabledFeatures.contains(FeatureConstants.VERBATIM_SEARCH);
     }
 
     /**
      * Returns whether the LIST_FILTER_QUERY_LANGUAGE feature is enabled.
      */
     public boolean isListFilterQueryLanguageEnabled() {
-        return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
+        return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE);
+    }
+
+    /**
+     * Returns whether the LIST_FILTER_HAS_PROPERTY_FUNCTION feature is enabled.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)
+    public boolean isListFilterHasPropertyFunctionEnabled() {
+        return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION);
+    }
+
+    /**
+     * Returns whether the embedding search feature is enabled.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public boolean isEmbeddingSearchEnabled() {
+        return mEnabledFeatures.contains(FeatureConstants.EMBEDDING_SEARCH);
+    }
+
+    /**
+     * Returns whether the LIST_FILTER_TOKENIZE_FUNCTION feature is enabled.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public boolean isListFilterTokenizeFunctionEnabled() {
+        return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
     }
 
     /**
@@ -539,21 +757,31 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public List<String> getEnabledFeatures() {
-        return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD);
+        return mEnabledFeatures;
+    }
+
+    @Override
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SearchSpecCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link SearchSpec objects}. */
     public static final class Builder {
-        private ArrayList<String> mSchemas = new ArrayList<>();
-        private ArrayList<String> mNamespaces = new ArrayList<>();
+        private List<String> mSchemas = new ArrayList<>();
+        private List<String> mNamespaces = new ArrayList<>();
         private Bundle mTypePropertyFilters = new Bundle();
-        private ArrayList<String> mPackageNames = new ArrayList<>();
+        private List<String> mPackageNames = new ArrayList<>();
         private ArraySet<String> mEnabledFeatures = new ArraySet<>();
         private Bundle mProjectionTypePropertyMasks = new Bundle();
         private Bundle mTypePropertyWeights = new Bundle();
+        private List<EmbeddingVector> mSearchEmbeddings = new ArrayList<>();
 
         private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
         @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;
+        @EmbeddingSearchMetricType
+        private int mDefaultEmbeddingSearchMetricType = EMBEDDING_SEARCH_METRIC_TYPE_COSINE;
         private int mSnippetCount = 0;
         private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
         private int mMaxSnippetSize = 0;
@@ -561,10 +789,53 @@
         @Order private int mOrder = ORDER_DESCENDING;
         @GroupingType private int mGroupingTypeFlags = 0;
         private int mGroupingLimit = 0;
-        private JoinSpec mJoinSpec;
+        @Nullable private JoinSpec mJoinSpec;
         private String mAdvancedRankingExpression = "";
+        private List<String> mInformationalRankingExpressions = new ArrayList<>();
+        @Nullable private String mSearchSourceLogTag;
         private boolean mBuilt = false;
 
+        /** Constructs a new builder for {@link SearchSpec} objects. */
+        public Builder() {
+        }
+
+        /** @exportToFramework:hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull SearchSpec searchSpec) {
+            Objects.requireNonNull(searchSpec);
+            mSchemas = new ArrayList<>(searchSpec.getFilterSchemas());
+            mNamespaces = new ArrayList<>(searchSpec.getFilterNamespaces());
+            for (Map.Entry<String, List<String>> entry :
+                    searchSpec.getFilterProperties().entrySet()) {
+                addFilterProperties(entry.getKey(), entry.getValue());
+            }
+            mPackageNames = new ArrayList<>(searchSpec.getFilterPackageNames());
+            mEnabledFeatures = new ArraySet<>(searchSpec.getEnabledFeatures());
+            for (Map.Entry<String, List<String>> entry : searchSpec.getProjections().entrySet()) {
+                addProjection(entry.getKey(), entry.getValue());
+            }
+            for (Map.Entry<String, Map<String, Double>> entry :
+                    searchSpec.getPropertyWeights().entrySet()) {
+                setPropertyWeights(entry.getKey(), entry.getValue());
+            }
+            mSearchEmbeddings = new ArrayList<>(searchSpec.getSearchEmbeddings());
+            mResultCountPerPage = searchSpec.getResultCountPerPage();
+            mTermMatchType = searchSpec.getTermMatch();
+            mDefaultEmbeddingSearchMetricType = searchSpec.getDefaultEmbeddingSearchMetricType();
+            mSnippetCount = searchSpec.getSnippetCount();
+            mSnippetCountPerProperty = searchSpec.getSnippetCountPerProperty();
+            mMaxSnippetSize = searchSpec.getMaxSnippetSize();
+            mRankingStrategy = searchSpec.getRankingStrategy();
+            mOrder = searchSpec.getOrder();
+            mGroupingTypeFlags = searchSpec.getResultGroupingTypeFlags();
+            mGroupingLimit = searchSpec.getResultGroupingLimit();
+            mJoinSpec = searchSpec.getJoinSpec();
+            mAdvancedRankingExpression = searchSpec.getAdvancedRankingExpression();
+            mInformationalRankingExpressions = new ArrayList<>(
+                    searchSpec.getInformationalRankingExpressions());
+            mSearchSourceLogTag = searchSpec.getSearchSourceLogTag();
+        }
+
         /**
          * Sets how the query terms should match {@code TermMatchCode} in the index.
          *
@@ -626,12 +897,13 @@
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
         public Builder addFilterDocumentClasses(
-                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+                @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+                throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
             List<String> schemas = new ArrayList<>(documentClasses.size());
             DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
-            for (Class<?> documentClass : documentClasses) {
+            for (java.lang.Class<?> documentClass : documentClasses) {
                 DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                 schemas.add(factory.getSchemaName());
             }
@@ -655,7 +927,7 @@
         @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+        public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
                 throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
@@ -685,17 +957,13 @@
          * @param schema the {@link AppSearchSchema} that contains the target properties
          * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
          *                      sequence of property names.
-         *
-         * @exportToFramework:hide
          */
-         // TODO(b/296088047) unhide from framework when type property filters are made public.
+        @CanIgnoreReturnValue
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
-        // @exportToFramework:endStrip()
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterProperties(@NonNull String schema,
                 @NonNull Collection<String> propertyPaths) {
             Preconditions.checkNotNull(schema);
@@ -720,17 +988,14 @@
          *
          * @param schema the {@link AppSearchSchema} that contains the target properties
          * @param propertyPaths The {@link PropertyPath} to search search over
-         *
-         * @exportToFramework:hide
          */
-         // TODO(b/296088047) unhide from framework when type property filters are made public.
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        // @exportToFramework:startStrip()
+        // Getter method is getFilterProperties
+        @SuppressLint("MissingGetterMatchingBuilder")
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
-        // @exportToFramework:endStrip()
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterPropertyPaths(@NonNull String schema,
                 @NonNull Collection<PropertyPath> propertyPaths) {
             Preconditions.checkNotNull(schema);
@@ -744,6 +1009,7 @@
 
 
 // @exportToFramework:startStrip()
+
         /**
          * Adds property paths for the specified type to the property filter of
          * {@link SearchSpec} Entry. Only returns documents that have matches under the specified
@@ -758,11 +1024,10 @@
          *
          */
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
-        public Builder addFilterProperties(@NonNull Class<?> documentClass,
+        public Builder addFilterProperties(@NonNull java.lang.Class<?> documentClass,
                 @NonNull Collection<String> propertyPaths) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             Preconditions.checkNotNull(propertyPaths);
@@ -787,11 +1052,12 @@
          *
          */
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        // Getter method is getFilterProperties
+        @SuppressLint("MissingGetterMatchingBuilder")
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
-        public Builder addFilterPropertyPaths(@NonNull Class<?> documentClass,
+        public Builder addFilterPropertyPaths(@NonNull java.lang.Class<?> documentClass,
                 @NonNull Collection<PropertyPath> propertyPaths) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             Preconditions.checkNotNull(propertyPaths);
@@ -966,6 +1232,19 @@
          *     current document being scored. Property weights come from what's specified in
          *     {@link SearchSpec}. After normalizing, each provided weight will be divided by the
          *     maximum weight, so that each of them will be <= 1.
+         *     <li>this.matchedSemanticScores(getSearchSpecEmbedding({embedding_index}), {metric})
+         *     <p>Returns a list of the matched similarity scores from "semanticSearch" in the query
+         *     expression (see also {@link AppSearchSession#search}) based on embedding_index and
+         *     metric. If metric is omitted, it defaults to the metric specified in
+         *     {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. If no
+         *     "semanticSearch" is called for embedding_index and metric in the query, this
+         *     function will return an empty list. If multiple "semanticSearch"s are called for
+         *     the same embedding_index and metric, this function will return a list of their
+         *     merged scores.
+         *     <p>Example: `this.matchedSemanticScores(getSearchSpecEmbedding(0), "COSINE")` will
+         *     return a list of matched scores within the range of [0.5, 1], if
+         *     `semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")` is called in the
+         *     query expression.
          * </ul>
          *
          * <p>Some errors may occur when using advanced ranking.
@@ -1011,11 +1290,9 @@
          */
         @CanIgnoreReturnValue
         @NonNull
-        // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION)
-        // @exportToFramework:endStrip()
         public Builder setRankingStrategy(@NonNull String advancedRankingExpression) {
             Preconditions.checkStringNotEmpty(advancedRankingExpression);
             resetIfBuilt();
@@ -1025,6 +1302,88 @@
         }
 
         /**
+         * Adds informational ranking expressions to be evaluated for each document in the search
+         * result. The values of these expressions will be returned to the caller via
+         * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for
+         * the caller to retrieve additional information about the result and have no effect on
+         * ranking.
+         *
+         * <p>The syntax is exactly the same as specified in
+         * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        public Builder addInformationalRankingExpressions(
+                @NonNull String... informationalRankingExpressions) {
+            Preconditions.checkNotNull(informationalRankingExpressions);
+            resetIfBuilt();
+            return addInformationalRankingExpressions(
+                    Arrays.asList(informationalRankingExpressions));
+        }
+
+        /**
+         * Adds informational ranking expressions to be evaluated for each document in the search
+         * result. The values of these expressions will be returned to the caller via
+         * {@link SearchResult#getInformationalRankingSignals()}. These expressions are purely for
+         * the caller to retrieve additional information about the result and have no effect on
+         * ranking.
+         *
+         * <p>The syntax is exactly the same as specified in
+         * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS)
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        public Builder addInformationalRankingExpressions(
+                @NonNull Collection<String> informationalRankingExpressions) {
+            Preconditions.checkNotNull(informationalRankingExpressions);
+            resetIfBuilt();
+            mInformationalRankingExpressions.addAll(informationalRankingExpressions);
+            return this;
+        }
+
+        /**
+         * Sets an optional log tag to indicate the source of this search.
+         *
+         * <p>Some AppSearch implementations may log a hash of this tag using statsd. This tag
+         * may be used for tracing performance issues and crashes to a component of an app.
+         *
+         * <p>Call this method and give a unique value if you want to distinguish this search
+         * scenario with other search scenarios during performance analysis.
+         *
+         * <p>Under no circumstances will AppSearch log the raw String value using statsd, but it
+         * will be provided as-is to custom {@code AppSearchLogger} implementations you have
+         * registered in your app.
+         *
+         * @param searchSourceLogTag A String to indicate the source caller of this search. It is
+         *                           used to label the search statsd for performance analysis. It
+         *                           is not the tag we are using in {@link android.util.Log}. The
+         *                           length of the teg should between 1 and 100.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
+        public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) {
+            Preconditions.checkStringNotEmpty(searchSourceLogTag);
+            Preconditions.checkArgument(searchSourceLogTag.length() <= 100,
+                    "The maximum supported tag length is 100. This tag is too long: "
+                            + searchSourceLogTag.length());
+            resetIfBuilt();
+            mSearchSourceLogTag = searchSourceLogTag;
+            return this;
+        }
+
+        /**
          * Sets the order of returned search results, the default is
          * {@link #ORDER_DESCENDING}, meaning that results with higher scores come first.
          *
@@ -1143,9 +1502,8 @@
          * results of that type will be retrieved.
          *
          * <p>If property path is added for the
-         * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
-         * apply to all results, excepting any types that have their own, specific property paths
-         * set.
+         * {@link SearchSpec#SCHEMA_TYPE_WILDCARD}, then those property paths will apply to all
+         * results, excepting any types that have their own, specific property paths set.
          *
          * <p>Suppose the following document is in the index.
          * <pre>{@code
@@ -1226,7 +1584,8 @@
         @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
         @NonNull
         public SearchSpec.Builder addProjectionsForDocumentClass(
-                @NonNull Class<?> documentClass, @NonNull Collection<String> propertyPaths)
+                @NonNull java.lang.Class<?> documentClass,
+                @NonNull Collection<String> propertyPaths)
                 throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             resetIfBuilt();
@@ -1247,7 +1606,8 @@
         @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
         @NonNull
         public SearchSpec.Builder addProjectionPathsForDocumentClass(
-                @NonNull Class<?> documentClass, @NonNull Collection<PropertyPath> propertyPaths)
+                @NonNull java.lang.Class<?> documentClass,
+                @NonNull Collection<PropertyPath> propertyPaths)
                 throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             resetIfBuilt();
@@ -1319,12 +1679,10 @@
          *                            weight to set for that property.
          * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
          */
-        // @exportToFramework:startStrip()
         @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
-        // @exportToFramework:endStrip()
         @NonNull
         public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType,
                 @NonNull Map<String, Double> propertyPathWeights) {
@@ -1354,12 +1712,10 @@
          *
          * @param joinSpec a specification on how to perform the Join operation.
          */
-        // @exportToFramework:startStrip()
         @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
-        // @exportToFramework:endStrip()
         @NonNull
         public Builder setJoinSpec(@NonNull JoinSpec joinSpec) {
             resetIfBuilt();
@@ -1397,12 +1753,10 @@
          *                            weight to set for that property.
          * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
          */
-        // @exportToFramework:startStrip()
         @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
-        // @exportToFramework:endStrip()
         @NonNull
         public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType,
                 @NonNull Map<PropertyPath, Double> propertyPathWeights) {
@@ -1460,7 +1814,7 @@
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
         @NonNull
         public SearchSpec.Builder setPropertyWeightsForDocumentClass(
-                @NonNull Class<?> documentClass,
+                @NonNull java.lang.Class<?> documentClass,
                 @NonNull Map<String, Double> propertyPathWeights) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
@@ -1508,7 +1862,7 @@
                 name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
         @NonNull
         public SearchSpec.Builder setPropertyWeightPathsForDocumentClass(
-                @NonNull Class<?> documentClass,
+                @NonNull java.lang.Class<?> documentClass,
                 @NonNull Map<PropertyPath, Double> propertyPathWeights) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
@@ -1518,6 +1872,72 @@
 // @exportToFramework:endStrip()
 
         /**
+         * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the
+         * query expression and the ranking expression for embedding search.
+         *
+         * @see AppSearchSession#search
+         * @see SearchSpec.Builder#setRankingStrategy(String)
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder addSearchEmbeddings(@NonNull EmbeddingVector... searchEmbeddings) {
+            Preconditions.checkNotNull(searchEmbeddings);
+            resetIfBuilt();
+            return addSearchEmbeddings(Arrays.asList(searchEmbeddings));
+        }
+
+        /**
+         * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the
+         * query expression and the ranking expression for embedding search.
+         *
+         * @see AppSearchSession#search
+         * @see SearchSpec.Builder#setRankingStrategy(String)
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder addSearchEmbeddings(
+                @NonNull Collection<EmbeddingVector> searchEmbeddings) {
+            Preconditions.checkNotNull(searchEmbeddings);
+            resetIfBuilt();
+            mSearchEmbeddings.addAll(searchEmbeddings);
+            return this;
+        }
+
+        /**
+         * Sets the default embedding metric type used for embedding search
+         * (see {@link AppSearchSession#search}) and ranking
+         * (see {@link SearchSpec.Builder#setRankingStrategy(String)}).
+         *
+         * <p>If this method is not called, the default embedding search metric type is
+         * {@link SearchSpec#EMBEDDING_SEARCH_METRIC_TYPE_COSINE}. Metrics specified within
+         * "semanticSearch" or "matchedSemanticScores" functions in search/ranking expressions
+         * will override this default.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder setDefaultEmbeddingSearchMetricType(
+                @EmbeddingSearchMetricType int defaultEmbeddingSearchMetricType) {
+            Preconditions.checkArgumentInRange(defaultEmbeddingSearchMetricType,
+                    EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+                    EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN, "Embedding search metric type");
+            resetIfBuilt();
+            mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+            return this;
+        }
+
+        /**
          * Sets the NUMERIC_SEARCH feature as enabled/disabled according to the enabled parameter.
          *
          * @param enabled Enables the feature if true, otherwise disables it.
@@ -1526,11 +1946,10 @@
          * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric
          * querying features.
          */
-        // @exportToFramework:startStrip()
+        @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.NUMERIC_SEARCH)
-        // @exportToFramework:endStrip()
         @NonNull
         public Builder setNumericSearchEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);
@@ -1550,11 +1969,10 @@
          * <p>For example, The verbatim string operator '"foo/bar" OR baz' will ensure that
          * 'foo/bar' is treated as a single 'verbatim' token.
          */
-        // @exportToFramework:startStrip()
+        @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.VERBATIM_SEARCH)
-        // @exportToFramework:endStrip()
         @NonNull
         public Builder setVerbatimSearchEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);
@@ -1578,7 +1996,7 @@
          * <p>The newly added custom functions covered by this feature are:
          * <ul>
          * <li>createList(String...)</li>
-         * <li>termSearch(String, List<String>)</li>
+         * <li>termSearch(String, {@code List<String>})</li>
          * </ul>
          *
          * <p>createList takes a variable number of strings and returns a list of strings.
@@ -1590,11 +2008,10 @@
          * for example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)"
          * could be rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))"
          */
-        // @exportToFramework:startStrip()
+        @CanIgnoreReturnValue
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.LIST_FILTER_QUERY_LANGUAGE)
-        // @exportToFramework:endStrip()
         @NonNull
         public Builder setListFilterQueryLanguageEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);
@@ -1602,6 +2019,65 @@
         }
 
         /**
+         * Sets the LIST_FILTER_HAS_PROPERTY_FUNCTION feature as enabled/disabled according to
+         * the enabled parameter.
+         *
+         * @param enabled Enables the feature if true, otherwise disables it
+         *
+         * <p>If disabled, disallows the use of the "hasProperty" function. See
+         * {@link AppSearchSession#search} for more details about the function.
+         */
+        @CanIgnoreReturnValue
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.LIST_FILTER_HAS_PROPERTY_FUNCTION)
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)
+        public Builder setListFilterHasPropertyFunctionEnabled(boolean enabled) {
+            modifyEnabledFeature(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION, enabled);
+            return this;
+        }
+
+        /**
+         * Sets the embedding search feature as enabled/disabled according to the enabled parameter.
+         *
+         * <p>If disabled, disallows the use of the "semanticSearch" function. See
+         * {@link AppSearchSession#search} for more details about the function.
+         *
+         * @param enabled Enables the feature if true, otherwise disables it
+         */
+        @CanIgnoreReturnValue
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder setEmbeddingSearchEnabled(boolean enabled) {
+            modifyEnabledFeature(FeatureConstants.EMBEDDING_SEARCH, enabled);
+            return this;
+        }
+
+        /**
+         * Sets the LIST_FILTER_TOKENIZE_FUNCTION feature as enabled/disabled according to
+         * the enabled parameter.
+         *
+         * @param enabled Enables the feature if true, otherwise disables it
+         *
+         * <p>If disabled, disallows the use of the "tokenize" function. See
+         * {@link AppSearchSession#search} for more details about the function.
+         */
+        @CanIgnoreReturnValue
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.LIST_FILTER_TOKENIZE_FUNCTION)
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+        public Builder setListFilterTokenizeFunctionEnabled(boolean enabled) {
+            modifyEnabledFeature(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION, enabled);
+            return this;
+        }
+
+        /**
          * Constructs a new {@link SearchSpec} from the contents of this builder.
          *
          * @throws IllegalArgumentException if property weights are provided with a
@@ -1617,7 +2093,6 @@
          */
         @NonNull
         public SearchSpec build() {
-            Bundle bundle = new Bundle();
             if (mJoinSpec != null) {
                 if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
                         && mJoinSpec.getAggregationScoringStrategy()
@@ -1626,59 +2101,26 @@
                             + "the nested JoinSpec, but ranking strategy is not "
                             + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE");
                 }
-                bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle());
             } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) {
                 throw new IllegalStateException("Attempting to rank based on joined documents, but "
                         + "no JoinSpec provided");
             }
             if (!mTypePropertyWeights.isEmpty()
-                    && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy
-                    && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {
+                    && mRankingStrategy != RANKING_STRATEGY_RELEVANCE_SCORE
+                    && mRankingStrategy != RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION) {
                 throw new IllegalArgumentException("Property weights are only compatible with the "
                         + "RANKING_STRATEGY_RELEVANCE_SCORE and "
                         + "RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies.");
             }
 
-            // If the schema filter isn't empty, and there is a schema with a projection but not
-            // in the filter, that is a SearchSpec user error.
-            if (!mSchemas.isEmpty()) {
-                for (String schema : mProjectionTypePropertyMasks.keySet()) {
-                    if (!mSchemas.contains(schema)) {
-                        throw new IllegalArgumentException("Projection requested for schema not "
-                                + "in schemas filters: " + schema);
-                    }
-                }
-            }
-
-            Set<String> schemaFilter = new ArraySet<>(mSchemas);
-            if (!mSchemas.isEmpty()) {
-                for (String schema : mTypePropertyFilters.keySet()) {
-                    if (!schemaFilter.contains(schema)) {
-                        throw new IllegalStateException(
-                                "The schema: " + schema + " exists in the property filter but "
-                                        + "doesn't exist in the schema filter.");
-                    }
-                }
-            }
-            bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
-            bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
-            bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
-            bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
-            bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures));
-            bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
-            bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage);
-            bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType);
-            bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount);
-            bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty);
-            bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize);
-            bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
-            bundle.putInt(ORDER_FIELD, mOrder);
-            bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
-            bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
-            bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);
-            bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression);
             mBuilt = true;
-            return new SearchSpec(bundle);
+            return new SearchSpec(mTermMatchType, mSchemas, mNamespaces,
+                    mTypePropertyFilters, mPackageNames, mResultCountPerPage,
+                    mRankingStrategy, mOrder, mSnippetCount, mSnippetCountPerProperty,
+                    mMaxSnippetSize, mProjectionTypePropertyMasks, mGroupingTypeFlags,
+                    mGroupingLimit, mTypePropertyWeights, mJoinSpec, mAdvancedRankingExpression,
+                    new ArrayList<>(mEnabledFeatures), mSearchSourceLogTag, mSearchEmbeddings,
+                    mDefaultEmbeddingSearchMetricType, mInformationalRankingExpressions);
         }
 
         private void resetIfBuilt() {
@@ -1689,6 +2131,9 @@
                 mPackageNames = new ArrayList<>(mPackageNames);
                 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
                 mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);
+                mSearchEmbeddings = new ArrayList<>(mSearchEmbeddings);
+                mInformationalRankingExpressions = new ArrayList<>(
+                        mInformationalRankingExpressions);
                 mBuilt = false;
             }
         }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
index 63fd696..c5171fd1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
@@ -16,38 +16,40 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionResultCreator;
 import androidx.core.util.Preconditions;
 
 /**
  * The result class of the {@link AppSearchSession#searchSuggestionAsync}.
  */
-public final class SearchSuggestionResult {
[email protected](creator = "SearchSuggestionResultCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSuggestionResult extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<SearchSuggestionResult> CREATOR =
+            new SearchSuggestionResultCreator();
 
-    private static final String SUGGESTED_RESULT_FIELD = "suggestedResult";
-    private final Bundle mBundle;
+    @Field(id = 1, getter = "getSuggestedResult")
+    private final String mSuggestedResult;
     @Nullable
     private Integer mHashCode;
 
-    SearchSuggestionResult(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     *
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    @Constructor
+    SearchSuggestionResult(@Param(id = 1) String suggestedResult) {
+        mSuggestedResult = Preconditions.checkNotNull(suggestedResult);
     }
 
     /**
@@ -60,7 +62,7 @@
      */
     @NonNull
     public String getSuggestedResult() {
-        return Preconditions.checkNotNull(mBundle.getString(SUGGESTED_RESULT_FIELD));
+        return mSuggestedResult;
     }
 
     @Override
@@ -72,13 +74,13 @@
             return false;
         }
         SearchSuggestionResult otherResult = (SearchSuggestionResult) other;
-        return BundleUtil.deepEquals(this.mBundle, otherResult.mBundle);
+        return mSuggestedResult.equals(otherResult.mSuggestedResult);
     }
 
     @Override
     public int hashCode() {
         if (mHashCode == null) {
-            mHashCode = BundleUtil.deepHashCode(mBundle);
+            mHashCode = mSuggestedResult.hashCode();
         }
         return mHashCode;
     }
@@ -102,12 +104,17 @@
             return this;
         }
 
-        /** Build a {@link SearchSuggestionResult} object*/
+        /** Build a {@link SearchSuggestionResult} object */
         @NonNull
         public SearchSuggestionResult build() {
-            Bundle bundle = new Bundle();
-            bundle.putString(SUGGESTED_RESULT_FIELD, mSuggestedResult);
-            return new SearchSuggestionResult(bundle);
+            return new SearchSuggestionResult(mSuggestedResult);
         }
     }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SearchSuggestionResultCreator.writeToParcel(this, dest, flags);
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
index 4b87f9c..f2db304 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
@@ -18,14 +18,22 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionSpecCreator;
 import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
@@ -47,24 +55,48 @@
  *
  * @see AppSearchSession#searchSuggestionAsync
  */
-public final class SearchSuggestionSpec {
-    static final String NAMESPACE_FIELD = "namespace";
-    static final String SCHEMA_FIELD = "schema";
-    static final String PROPERTY_FIELD = "property";
-    static final String DOCUMENT_IDS_FIELD = "documentIds";
-    static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";
-    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
-    private final Bundle mBundle;
[email protected](creator = "SearchSuggestionSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SearchSuggestionSpec extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<SearchSuggestionSpec> CREATOR =
+            new SearchSuggestionSpecCreator();
+    @Field(id = 1, getter = "getFilterNamespaces")
+    private final List<String> mFilterNamespaces;
+    @Field(id = 2, getter = "getFilterSchemas")
+    private final List<String> mFilterSchemas;
+    // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
+    // schema type and value is a list of target property paths in that schema to search over.
+    @Field(id = 3)
+    final Bundle mFilterProperties;
+    // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
+    // namespace and value is a list of target document ids in that namespace to search over.
+    @Field(id = 4)
+    final Bundle mFilterDocumentIds;
+    @Field(id = 5, getter = "getRankingStrategy")
+    private final int mRankingStrategy;
+    @Field(id = 6, getter = "getMaximumResultCount")
     private final int mMaximumResultCount;
 
     /** @exportToFramework:hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public SearchSuggestionSpec(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-        mMaximumResultCount = bundle.getInt(MAXIMUM_RESULT_COUNT_FIELD);
-        Preconditions.checkArgument(mMaximumResultCount >= 1,
+    @Constructor
+    public SearchSuggestionSpec(
+            @Param(id = 1) @NonNull List<String> filterNamespaces,
+            @Param(id = 2) @NonNull List<String> filterSchemas,
+            @Param(id = 3) @NonNull Bundle filterProperties,
+            @Param(id = 4) @NonNull Bundle filterDocumentIds,
+            @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy,
+            @Param(id = 6) int maximumResultCount) {
+        Preconditions.checkArgument(maximumResultCount >= 1,
                 "MaximumResultCount must be positive.");
+        mFilterNamespaces = Preconditions.checkNotNull(filterNamespaces);
+        mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
+        mFilterProperties = Preconditions.checkNotNull(filterProperties);
+        mFilterDocumentIds = Preconditions.checkNotNull(filterDocumentIds);
+        mRankingStrategy = rankingStrategy;
+        mMaximumResultCount = maximumResultCount;
     }
 
     /**
@@ -111,17 +143,6 @@
     public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2;
 
     /**
-     * Returns the {@link Bundle} populated by this builder.
-     *
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
-    }
-
-    /**
      * Returns the maximum number of wanted suggestion that will be returned in the result object.
      */
     public int getMaximumResultCount() {
@@ -135,17 +156,16 @@
      */
     @NonNull
     public List<String> getFilterNamespaces() {
-        List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
-        if (namespaces == null) {
+        if (mFilterNamespaces == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(namespaces);
+        return Collections.unmodifiableList(mFilterNamespaces);
     }
 
     /** Returns the ranking strategy. */
     @SuggestionRankingStrategy
     public int getRankingStrategy() {
-        return mBundle.getInt(RANKING_STRATEGY_FIELD);
+        return mRankingStrategy;
     }
 
     /**
@@ -155,11 +175,10 @@
      */
     @NonNull
     public List<String> getFilterSchemas() {
-        List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_FIELD);
-        if (schemaTypes == null) {
+        if (mFilterSchemas == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(schemaTypes);
+        return Collections.unmodifiableList(mFilterSchemas);
     }
 
     /**
@@ -173,20 +192,15 @@
      *
      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
      * by this function, rather than calling it multiple times.
-     *
-     * @exportToFramework:hide
      */
-    // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
     @NonNull
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
     public Map<String, List<String>> getFilterProperties() {
-        Bundle typePropertyPathsBundle = Preconditions.checkNotNull(
-                mBundle.getBundle(PROPERTY_FIELD));
-        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Set<String> schemas = mFilterProperties.keySet();
         Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
         for (String schema : schemas) {
             typePropertyPathsMap.put(schema, Preconditions.checkNotNull(
-                    typePropertyPathsBundle.getStringArrayList(schema)));
+                    mFilterProperties.getStringArrayList(schema)));
         }
         return typePropertyPathsMap;
     }
@@ -205,13 +219,11 @@
      */
     @NonNull
     public Map<String, List<String>> getFilterDocumentIds() {
-        Bundle documentIdsBundle = Preconditions.checkNotNull(
-                mBundle.getBundle(DOCUMENT_IDS_FIELD));
-        Set<String> namespaces = documentIdsBundle.keySet();
+        Set<String> namespaces = mFilterDocumentIds.keySet();
         Map<String, List<String>> documentIdsMap = new ArrayMap<>(namespaces.size());
         for (String namespace : namespaces) {
             documentIdsMap.put(namespace, Preconditions.checkNotNull(
-                    documentIdsBundle.getStringArrayList(namespace)));
+                    mFilterDocumentIds.getStringArrayList(namespace)));
         }
         return documentIdsMap;
     }
@@ -328,7 +340,7 @@
         @SuppressLint("MissingGetterMatchingBuilder")
         @CanIgnoreReturnValue
         @NonNull
-        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+        public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
                 throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
@@ -353,12 +365,13 @@
         @CanIgnoreReturnValue
         @NonNull
         public Builder addFilterDocumentClasses(
-                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+                @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+                throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
             List<String> schemas = new ArrayList<>(documentClasses.size());
             DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
-            for (Class<?> documentClass : documentClasses) {
+            for (java.lang.Class<?> documentClass : documentClasses) {
                 DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                 schemas.add(factory.getSchemaName());
             }
@@ -385,11 +398,13 @@
          * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited
          *                      sequence of property names indicating which property in the
          *                      document these snippets correspond to.
-         * @exportToFramework:hide
          */
-        // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+        @CanIgnoreReturnValue
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterProperties(@NonNull String schema,
                 @NonNull Collection<String> propertyPaths) {
             Preconditions.checkNotNull(schema);
@@ -418,12 +433,15 @@
          *
          * @param schema the {@link AppSearchSchema} that contains the target properties
          * @param propertyPaths The {@link PropertyPath} to search suggestion over
-         *
-         * @exportToFramework:hide
          */
-        // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
+        @CanIgnoreReturnValue
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        // Getter method is getFilterProperties
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterPropertyPaths(@NonNull String schema,
                 @NonNull Collection<PropertyPath> propertyPaths) {
             Preconditions.checkNotNull(schema);
@@ -453,12 +471,12 @@
          * @param propertyPaths The String version of {@link PropertyPath}. A
          * {@code dot-delimited sequence of property names indicating which property in the
          * document these snippets correspond to.
-         * @exportToFramework:hide
          */
-        // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public Builder addFilterProperties(@NonNull Class<?> documentClass,
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        public Builder addFilterProperties(@NonNull java.lang.Class<?> documentClass,
                 @NonNull Collection<String> propertyPaths) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             Preconditions.checkNotNull(propertyPaths);
@@ -484,12 +502,14 @@
          *
          * @param documentClass class annotated with {@link Document}.
          * @param propertyPaths The {@link PropertyPath} to search suggestion over
-         * @exportToFramework:hide
          */
-        // TODO(b/228240987) migrate this API when we support property restrict for multiple terms
         @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public Builder addFilterPropertyPaths(@NonNull Class<?> documentClass,
+        // Getter method is getFilterProperties
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES)
+        public Builder addFilterPropertyPaths(@NonNull java.lang.Class<?> documentClass,
                 @NonNull Collection<PropertyPath> propertyPaths) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             Preconditions.checkNotNull(propertyPaths);
@@ -540,7 +560,6 @@
         /** Constructs a new {@link SearchSpec} from the contents of this builder. */
         @NonNull
         public SearchSuggestionSpec build() {
-            Bundle bundle = new Bundle();
             if (!mSchemas.isEmpty()) {
                 Set<String> schemaFilter = new ArraySet<>(mSchemas);
                 for (String schema : mTypePropertyFilters.keySet()) {
@@ -561,14 +580,14 @@
                     }
                 }
             }
-            bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
-            bundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
-            bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters);
-            bundle.putBundle(DOCUMENT_IDS_FIELD, mDocumentIds);
-            bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount);
-            bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
             mBuilt = true;
-            return new SearchSuggestionSpec(bundle);
+            return new SearchSuggestionSpec(
+                    mNamespaces,
+                    mSchemas,
+                    mTypePropertyFilters,
+                    mDocumentIds,
+                    mRankingStrategy,
+                    mTotalResultCount);
         }
 
         private void resetIfBuilt() {
@@ -581,4 +600,11 @@
             }
         }
     }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SearchSuggestionSpecCreator.writeToParcel(this, dest, flags);
+    }
 }
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 6cc0e85..3cabaab 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -21,10 +21,13 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -100,13 +103,13 @@
             READ_EXTERNAL_STORAGE,
             READ_HOME_APP_SEARCH_DATA,
             READ_ASSISTANT_APP_SEARCH_DATA,
+            ENTERPRISE_ACCESS,
+            MANAGED_PROFILE_CONTACTS_ACCESS,
     })
     @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 {}
 
@@ -114,72 +117,85 @@
      * 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;
 
+    /**
+     * A schema must have this permission set through {@link
+     * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an
+     * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will
+     * not count as having this permission.
+     *
+     * @exportToFramework:hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final int ENTERPRISE_ACCESS = 7;
+
+    /**
+     * A schema with this permission set through {@link
+     * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller
+     * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to
+     * be visible. This permission indicates that the protected schema may expose managed profile
+     * data for contacts search.
+     *
+     * @exportToFramework:hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
+
     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, PackageIdentifier> mPubliclyVisibleSchemas;
+    private final Map<String, Set<SchemaVisibilityConfig>> mSchemasVisibleToConfigs;
     private final Map<String, Migrator> mMigrators;
     private final boolean mForceOverride;
     private final int mVersion;
@@ -188,6 +204,8 @@
             @NonNull Set<String> schemasNotDisplayedBySystem,
             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
             @NonNull Map<String, Set<Set<Integer>>> schemasVisibleToPermissions,
+            @NonNull Map<String, PackageIdentifier> publiclyVisibleSchemas,
+            @NonNull Map<String, Set<SchemaVisibilityConfig>> schemasVisibleToConfigs,
             @NonNull Map<String, Migrator> migrators,
             boolean forceOverride,
             int version) {
@@ -195,6 +213,8 @@
         mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
         mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
         mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);
+        mPubliclyVisibleSchemas = Preconditions.checkNotNull(publiclyVisibleSchemas);
+        mSchemasVisibleToConfigs = Preconditions.checkNotNull(schemasVisibleToConfigs);
         mMigrators = Preconditions.checkNotNull(migrators);
         mForceOverride = forceOverride;
         mVersion = version;
@@ -258,12 +278,41 @@
      *         {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
      *         {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
      */
+    // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
     @NonNull
     public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
         return deepCopy(mSchemasVisibleToPermissions);
     }
 
     /**
+     * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying
+     * the package the schemas are from.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+    @NonNull
+    public Map<String, PackageIdentifier> getPubliclyVisibleSchemas() {
+        return Collections.unmodifiableMap(mPubliclyVisibleSchemas);
+    }
+
+    /**
+     * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
+     * access to that schema type.
+     *
+     * <p>It’s inefficient to call this method repeatedly.
+     * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+    @NonNull
+    public Map<String, Set<SchemaVisibilityConfig>> getSchemasVisibleToConfigs() {
+        Map<String, Set<SchemaVisibilityConfig>> copy = new ArrayMap<>();
+        for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                mSchemasVisibleToConfigs.entrySet()) {
+            copy.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+        }
+        return copy;
+    }
+
+    /**
      * Returns the map of {@link Migrator}, the key will be the schema type of the
      * {@link Migrator} associated with.
      */
@@ -307,6 +356,9 @@
         private ArrayMap<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
                 new ArrayMap<>();
         private ArrayMap<String, Set<Set<Integer>>> mSchemasVisibleToPermissions = new ArrayMap<>();
+        private ArrayMap<String, PackageIdentifier> mPubliclyVisibleSchemas = new ArrayMap<>();
+        private ArrayMap<String, Set<SchemaVisibilityConfig>> mSchemaVisibleToConfigs =
+                new ArrayMap<>();
         private ArrayMap<String, Migrator> mMigrators = new ArrayMap<>();
         private boolean mForceOverride = false;
         private int mVersion = DEFAULT_VERSION;
@@ -451,10 +503,16 @@
          * <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},
+         * <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}.
          *
+         * <p> The relationship between permissions added in this method and package visibility
+         * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access
+         * the schema if they match ANY requirements. If you want to set "AND" requirements like
+         * a caller must hold required permissions AND it is a specified package, please use
+         * {@link #addSchemaTypeVisibleToConfig}.
+         *
          * @see android.Manifest.permission#READ_SMS
          * @see android.Manifest.permission#READ_CALENDAR
          * @see android.Manifest.permission#READ_CONTACTS
@@ -467,14 +525,13 @@
          *                         schema.
          * @throws IllegalArgumentException – if input unsupported permission.
          */
+        // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
         // Merged list available from getRequiredPermissionsForSchemaTypeVisibility
         @CanIgnoreReturnValue
         @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) {
@@ -482,7 +539,7 @@
             Preconditions.checkNotNull(permissions);
             for (int permission : permissions) {
                 Preconditions.checkArgumentInRange(permission, READ_SMS,
-                        READ_ASSISTANT_APP_SEARCH_DATA, "permission");
+                        MANAGED_PROFILE_CONTACTS_ACCESS, "permission");
             }
             resetIfBuilt();
             Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
@@ -495,12 +552,10 @@
         }
 
         /**  Clears all required permissions combinations for the given schema type.  */
-        // @exportToFramework:startStrip()
         @CanIgnoreReturnValue
         @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);
@@ -525,6 +580,12 @@
          *
          * <p>By default, data sharing between applications is disabled.
          *
+         * <p> The relationship between permissions added in this method and package visibility
+         * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access
+         * the schema if they match ANY requirements. If you want to set "AND" requirements like
+         * a caller must hold required permissions AND it is a specified package, please use
+         * {@link #addSchemaTypeVisibleToConfig}.
+         *
          * @param schemaType        The schema type to set visibility on.
          * @param visible           Whether the {@code schemaType} will be visible or not.
          * @param packageIdentifier Represents the package that will be granted visibility.
@@ -564,6 +625,132 @@
         }
 
         /**
+         * Specify that the schema should be publicly available, to packages which already have
+         * visibility to {@code packageIdentifier}. This visibility is determined by the result of
+         * {@link android.content.pm.PackageManager#canPackageQuery}.
+         *
+         * <p> It is possible for the packageIdentifier parameter to be different from the
+         * package performing the indexing. This might happen in the case of an on-device indexer
+         * processing information about various packages. The visibility will be the same
+         * regardless of which package indexes the document, as the visibility is based on the
+         * packageIdentifier parameter.
+         *
+         * <p> If this is called repeatedly with the same schema, the {@link PackageIdentifier} in
+         * the last call will be used as the "from" package for that schema.
+         *
+         * <p> Calling this with packageIdentifier set to null is valid, and will remove public
+         * visibility for the schema.
+         *
+         * @param schema the schema to make publicly accessible.
+         * @param packageIdentifier if an app can see this package via
+         *                          PackageManager#canPackageQuery, it will be able to see the
+         *                          documents of type {@code schema}.
+         */
+        // Merged list available from getPubliclyVisibleSchemas
+        @CanIgnoreReturnValue
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+        @NonNull
+        public Builder setPubliclyVisibleSchema(@NonNull String schema,
+                @Nullable PackageIdentifier packageIdentifier) {
+            Preconditions.checkNotNull(schema);
+            resetIfBuilt();
+
+            // If the package identifier is null or empty we clear public visibility
+            if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) {
+                mPubliclyVisibleSchemas.remove(schema);
+                return this;
+            }
+
+            mPubliclyVisibleSchemas.put(schema, packageIdentifier);
+            return this;
+        }
+
+// @exportToFramework:startStrip()
+        /**
+         * Specify that the schema should be publicly available, to packages which already have
+         * visibility to {@code packageIdentifier}.
+         *
+         * @param documentClass the document to make publicly accessible.
+         * @param packageIdentifier if an app can see this package via
+         *                          PackageManager#canPackageQuery, it will be able to see the
+         *                          documents of type {@code documentClass}.
+         * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema
+         */
+        // Merged list available from getPubliclyVisibleSchemas
+        @CanIgnoreReturnValue
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+        @NonNull
+        public Builder setPubliclyVisibleDocumentClass(@NonNull Class<?> documentClass,
+                @Nullable PackageIdentifier packageIdentifier) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setPubliclyVisibleSchema(factory.getSchemaName(), packageIdentifier);
+        }
+// @exportToFramework:endStrip()
+
+        /**
+         * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+         * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+         *
+         * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+         * caller must match ALL requirements to access the schema. For example, a caller must hold
+         * required permissions AND it is a specified package.
+         *
+         * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+         * and the querier will have access if they match ANY of the
+         * {@link SchemaVisibilityConfig}.
+         *
+         * @param schemaType              The schema type to set visibility on.
+         * @param schemaVisibilityConfig  The {@link SchemaVisibilityConfig} holds all requirements
+         *                                that a call must to match to access the schema.
+         */
+        // Merged list available from getSchemasVisibleToConfigs
+        @CanIgnoreReturnValue
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+        @NonNull
+        public Builder addSchemaTypeVisibleToConfig(@NonNull String schemaType,
+                @NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(schemaVisibilityConfig);
+            resetIfBuilt();
+            Set<SchemaVisibilityConfig> visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType);
+            if (visibleToConfigs == null) {
+                visibleToConfigs = new ArraySet<>();
+                mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs);
+            }
+            visibleToConfigs.add(schemaVisibilityConfig);
+            return this;
+        }
+
+        /**  Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
+        @CanIgnoreReturnValue
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+        @NonNull
+        public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            resetIfBuilt();
+            mSchemaVisibleToConfigs.remove(schemaType);
+            return this;
+        }
+
+        /**
          * Sets the {@link Migrator} associated with the given SchemaType.
          *
          * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
@@ -684,6 +871,12 @@
          *
          * <p>By default, app data sharing between applications is disabled.
          *
+         * <p> The relationship between visible packages added in this method and permission
+         * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The
+         * caller could access the schema if they match ANY requirements. If you want to set
+         * "AND" requirements like a caller must hold required permissions AND it is a specified
+         * package, please use {@link #addSchemaTypeVisibleToConfig}.
+         *
          * <p>Merged list available from {@link #getSchemasVisibleToPackages()}.
          *
          * @param documentClass     The {@link androidx.appsearch.annotation.Document} class to set
@@ -721,6 +914,12 @@
          * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE},
          * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}.
          *
+         * <p> The relationship between visible packages added in this method and permission
+         * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The
+         * caller could access the schema if they match ANY requirements. If you want to set
+         * "AND" requirements like a caller must hold required permissions AND it is a specified
+         * package, please use {@link #addSchemaTypeVisibleToConfig}.
+         *
          * <p>Merged map available from {@link #getRequiredPermissionsForSchemaTypeVisibility()}.
          * @see android.Manifest.permission#READ_SMS
          * @see android.Manifest.permission#READ_CALENDAR
@@ -768,6 +967,58 @@
             DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
             return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName());
         }
+
+        /**
+         * Sets the documents from the provided {@code schemaType} can be read by the caller if they
+         * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}.
+         *
+         * <p> The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A
+         * caller must match ALL requirements to access the schema. For example, a caller must hold
+         * required permissions AND it is a specified package.
+         *
+         * <p> You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s,
+         * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}.
+         *
+         * @param documentClass            A class annotated with
+         *                                 {@link androidx.appsearch.annotation.Document}, the
+         *                                 visibility of which will be configured
+         * @param schemaVisibilityConfig   The {@link SchemaVisibilityConfig} holds all
+         *                                 requirements that a call must to match to access the
+         *                                 schema.
+         */
+        // Merged list available from getSchemasVisibleToConfigs
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+        @NonNull
+        public Builder addDocumentClassVisibleToConfig(
+                @NonNull Class<?> documentClass,
+                @NonNull SchemaVisibilityConfig schemaVisibilityConfig)
+                throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return addSchemaTypeVisibleToConfig(factory.getSchemaName(),
+                    schemaVisibilityConfig);
+        }
+
+        /**  Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG)
+        @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
+        @NonNull
+        public Builder clearDocumentClassVisibleToConfigs(
+                @NonNull Class<?> documentClass) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return clearSchemaTypeVisibleToConfigs(factory.getSchemaName());
+        }
 // @exportToFramework:endStrip()
 
         /**
@@ -841,6 +1092,8 @@
             Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
             referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet());
+            referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet());
+            referencedSchemas.addAll(mSchemaVisibleToConfigs.keySet());
 
             for (AppSearchSchema schema : mSchemas) {
                 referencedSchemas.remove(schema.getSchemaType());
@@ -861,6 +1114,8 @@
                     mSchemasNotDisplayedBySystem,
                     mSchemasVisibleToPackages,
                     mSchemasVisibleToPermissions,
+                    mPubliclyVisibleSchemas,
+                    mSchemaVisibleToConfigs,
                     mMigrators,
                     mForceOverride,
                     mVersion);
@@ -876,8 +1131,18 @@
                 }
                 mSchemasVisibleToPackages = schemasVisibleToPackages;
 
+                mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas);
+
                 mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions);
 
+                ArrayMap<String, Set<SchemaVisibilityConfig>> schemaVisibleToConfigs =
+                        new ArrayMap<>(mSchemaVisibleToConfigs.size());
+                for (Map.Entry<String, Set<SchemaVisibilityConfig>> entry :
+                        mSchemaVisibleToConfigs.entrySet()) {
+                    schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue()));
+                }
+                mSchemaVisibleToConfigs = schemaVisibleToConfigs;
+
                 mSchemas = new ArraySet<>(mSchemas);
                 mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem);
                 mMigrators = new ArrayMap<>(mMigrators);
@@ -886,8 +1151,8 @@
         }
     }
 
-    static ArrayMap<String, Set<Set<Integer>>> deepCopy(@NonNull Map<String,
-            Set<Set<Integer>>> original) {
+    private 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<>();
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 07d087a..85866fd 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -16,12 +16,19 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.MigrationFailureCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.SetSchemaResponseCreator;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -32,57 +39,70 @@
 import java.util.Set;
 
 /** The response class of {@link AppSearchSession#setSchemaAsync} */
-public class SetSchemaResponse {
[email protected](creator = "SetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class SetSchemaResponse extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator<SetSchemaResponse> CREATOR =
+            new SetSchemaResponseCreator();
 
-    private static final String DELETED_TYPES_FIELD = "deletedTypes";
-    private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
-    private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
+    @Field(id = 1)
+    final List<String> mDeletedTypes;
+    @Field(id = 2)
+    final List<String> mIncompatibleTypes;
+    @Field(id = 3)
+    final List<String> mMigratedTypes;
 
-    private final Bundle mBundle;
     /**
-     * The migrationFailures won't be saved in the bundle. Since:
+     * The migrationFailures won't be saved as a SafeParcelable field. Since:
      * <ul>
      *     <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
-     *         the SDK side in platform. We don't need to pass it from service side via binder.
-     *     <li>Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
-     *         back in constructor will be a huge waste.
+     *         the SDK side in platform. We don't need to pass it from service side via binder as
+     *         a part of {@link SetSchemaResponse}.
+     *     <li>Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and
+     *     then back in constructor will be a huge waste.
      * </ul>
      */
     private final List<MigrationFailure> mMigrationFailures;
 
-    /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */
-    @Nullable
-    private Set<String> mDeletedTypes;
+    /** Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use
+     */
+    @Nullable private Set<String> mDeletedTypesCached;
 
-    /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */
-    @Nullable
-    private Set<String> mMigratedTypes;
+    /** Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first
+     *  use.
+     */
+    @Nullable private Set<String> mMigratedTypesCached;
 
     /**
-     * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.
+     * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at
+     * first use.
      */
-    @Nullable
-    private Set<String> mIncompatibleTypes;
+    @Nullable private Set<String> mIncompatibleTypesCached;
 
-    SetSchemaResponse(@NonNull Bundle bundle, @NonNull List<MigrationFailure> migrationFailures) {
-        mBundle = Preconditions.checkNotNull(bundle);
+    @Constructor
+    SetSchemaResponse(
+            @Param(id = 1) @NonNull List<String> deletedTypes,
+            @Param(id = 2) @NonNull List<String> incompatibleTypes,
+            @Param(id = 3) @NonNull List<String> migratedTypes) {
+        mDeletedTypes = deletedTypes;
+        mIncompatibleTypes = incompatibleTypes;
+        mMigratedTypes = migratedTypes;
+        mMigrationFailures = Collections.emptyList();
+    }
+
+    SetSchemaResponse(
+            @NonNull List<String> deletedTypes,
+            @NonNull List<String> incompatibleTypes,
+            @NonNull List<String> migratedTypes,
+            @NonNull List<MigrationFailure> migrationFailures) {
+        mDeletedTypes = deletedTypes;
+        mIncompatibleTypes = incompatibleTypes;
+        mMigratedTypes = migratedTypes;
         mMigrationFailures = Preconditions.checkNotNull(migrationFailures);
     }
 
-    SetSchemaResponse(@NonNull Bundle bundle) {
-        this(bundle, /*migrationFailures=*/ Collections.emptyList());
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
-     */
-    @NonNull
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public Bundle getBundle() {
-        return mBundle;
-    }
-
     /**
      * Returns a {@link List} of all failed {@link MigrationFailure}.
      *
@@ -109,11 +129,10 @@
      */
     @NonNull
     public Set<String> getDeletedTypes() {
-        if (mDeletedTypes == null) {
-            mDeletedTypes = new ArraySet<>(
-                    Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD)));
+        if (mDeletedTypesCached == null) {
+            mDeletedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mDeletedTypes));
         }
-        return Collections.unmodifiableSet(mDeletedTypes);
+        return Collections.unmodifiableSet(mDeletedTypesCached);
     }
 
     /**
@@ -131,11 +150,10 @@
      */
     @NonNull
     public Set<String> getMigratedTypes() {
-        if (mMigratedTypes == null) {
-            mMigratedTypes = new ArraySet<>(
-                    Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));
+        if (mMigratedTypesCached == null) {
+            mMigratedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mMigratedTypes));
         }
-        return Collections.unmodifiableSet(mMigratedTypes);
+        return Collections.unmodifiableSet(mMigratedTypesCached);
     }
 
     /**
@@ -151,27 +169,11 @@
      */
     @NonNull
     public Set<String> getIncompatibleTypes() {
-        if (mIncompatibleTypes == null) {
-            mIncompatibleTypes = new ArraySet<>(
-                    Preconditions.checkNotNull(
-                            mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));
+        if (mIncompatibleTypesCached == null) {
+            mIncompatibleTypesCached =
+                    new ArraySet<>(Preconditions.checkNotNull(mIncompatibleTypes));
         }
-        return Collections.unmodifiableSet(mIncompatibleTypes);
-    }
-
-    /**
-     * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy
-    public Builder toBuilder() {
-        return new Builder()
-                .addDeletedTypes(getDeletedTypes())
-                .addIncompatibleTypes(getIncompatibleTypes())
-                .addMigratedTypes(getMigratedTypes())
-                .addMigrationFailures(mMigrationFailures);
+        return Collections.unmodifiableSet(mIncompatibleTypesCached);
     }
 
     /** Builder for {@link SetSchemaResponse} objects. */
@@ -182,6 +184,23 @@
         private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
         private boolean mBuilt = false;
 
+        /**
+         * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse.
+         *
+         * @exportToFramework:hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull SetSchemaResponse setSchemaResponse) {
+            Preconditions.checkNotNull(setSchemaResponse);
+            mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes());
+            mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes());
+            mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes());
+            mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures());
+        }
+
+        /** Create a {@link Builder} object} */
+        public Builder() {}
+
         /**  Adds {@link MigrationFailure}s to the list of migration failures. */
         @CanIgnoreReturnValue
         @NonNull
@@ -266,15 +285,15 @@
         /** Builds a {@link SetSchemaResponse} object. */
         @NonNull
         public SetSchemaResponse build() {
-            Bundle bundle = new Bundle();
-            bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);
-            bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);
-            bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes);
             mBuilt = true;
             // Avoid converting the potential thousands of MigrationFailures to Pracelable and
             // back just for put in bundle. In platform, we should set MigrationFailures in
             // AppSearchSession after we pass SetSchemaResponse via binder.
-            return new SetSchemaResponse(bundle, mMigrationFailures);
+            return new SetSchemaResponse(
+                    mDeletedTypes,
+                    mIncompatibleTypes,
+                    mMigratedTypes,
+                    mMigrationFailures);
         }
 
         private void resetIfBuilt() {
@@ -288,18 +307,50 @@
         }
     }
 
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        SetSchemaResponseCreator.writeToParcel(this, dest, flags);
+    }
+
     /**
      * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
      * {@link AppSearchSession#setSchemaAsync}.
      */
-    public static class MigrationFailure {
-        private static final String SCHEMA_TYPE_FIELD = "schemaType";
-        private static final String NAMESPACE_FIELD = "namespace";
-        private static final String DOCUMENT_ID_FIELD = "id";
-        private static final String ERROR_MESSAGE_FIELD = "errorMessage";
-        private static final String RESULT_CODE_FIELD = "resultCode";
+    @SafeParcelable.Class(creator = "MigrationFailureCreator")
+    @SuppressWarnings("HiddenSuperclass")
+    public static class MigrationFailure extends AbstractSafeParcelable {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+        @NonNull
+        public static final Parcelable.Creator<MigrationFailure> CREATOR =
+                new MigrationFailureCreator();
 
-        private final Bundle mBundle;
+        @Field(id = 1, getter = "getNamespace")
+        private final String mNamespace;
+        @Field(id = 2, getter = "getDocumentId")
+        private final String mDocumentId;
+        @Field(id = 3, getter = "getSchemaType")
+        private final String mSchemaType;
+        @Field(id = 4)
+        @Nullable final String mErrorMessage;
+        @Field(id = 5)
+        final int mResultCode;
+
+        @Constructor
+        MigrationFailure(
+                @Param(id = 1) @NonNull String namespace,
+                @Param(id = 2) @NonNull String documentId,
+                @Param(id = 3) @NonNull String schemaType,
+                @Param(id = 4) @Nullable String errorMessage,
+                @Param(id = 5) int resultCode) {
+            mNamespace = namespace;
+            mDocumentId = documentId;
+            mSchemaType = schemaType;
+            mErrorMessage = errorMessage;
+            mResultCode = resultCode;
+        }
 
         /**
          * Constructs a new {@link MigrationFailure}.
@@ -315,49 +366,33 @@
                 @NonNull String documentId,
                 @NonNull String schemaType,
                 @NonNull AppSearchResult<?> failedResult) {
-            mBundle = new Bundle();
-            mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace));
-            mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId));
-            mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType));
+            mNamespace = namespace;
+            mDocumentId = documentId;
+            mSchemaType = schemaType;
 
             Preconditions.checkNotNull(failedResult);
             Preconditions.checkArgument(
                     !failedResult.isSuccess(), "failedResult was actually successful");
-            mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());
-            mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());
-        }
-
-        MigrationFailure(@NonNull Bundle bundle) {
-            mBundle = Preconditions.checkNotNull(bundle);
-        }
-
-        /**
-         * Returns the Bundle of the {@link MigrationFailure}.
-         *
-         * @exportToFramework:hide
-         */
-        @NonNull
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public Bundle getBundle() {
-            return mBundle;
+            mErrorMessage = failedResult.getErrorMessage();
+            mResultCode = failedResult.getResultCode();
         }
 
         /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
         @NonNull
         public String getNamespace() {
-            return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/"");
+            return mNamespace;
         }
 
         /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
         @NonNull
         public String getDocumentId() {
-            return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/"");
+            return mDocumentId;
         }
 
         /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
         @NonNull
         public String getSchemaType() {
-            return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/"");
+            return mSchemaType;
         }
 
         /**
@@ -366,8 +401,7 @@
          */
         @NonNull
         public AppSearchResult<Void> getAppSearchResult() {
-            return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD),
-                    mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/""));
+            return AppSearchResult.newFailedResult(mResultCode, mErrorMessage);
         }
 
         @NonNull
@@ -377,5 +411,12 @@
                     + getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: "
                     + getAppSearchResult().toString() + "}";
         }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            MigrationFailureCreator.writeToParcel(this, dest, flags);
+        }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
index d95eff6..fd22846 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -16,39 +16,49 @@
 
 package androidx.appsearch.app;
 
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.StorageInfoCreator;
 
 /** The response class of {@code AppSearchSession#getStorageInfo}. */
-public class StorageInfo {
-
-    private static final String SIZE_BYTES_FIELD = "sizeBytes";
-    private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
-    private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
-
-    private final Bundle mBundle;
-
-    StorageInfo(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
-     */
-    @NonNull
[email protected](creator = "StorageInfoCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class StorageInfo extends AbstractSafeParcelable {
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public Bundle getBundle() {
-        return mBundle;
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<StorageInfo> CREATOR = new StorageInfoCreator();
+
+    @Field(id = 1, getter = "getSizeBytes")
+    private long mSizeBytes;
+
+    @Field(id = 2, getter = "getAliveDocumentsCount")
+    private int mAliveDocumentsCount;
+
+    @Field(id = 3, getter = "getAliveNamespacesCount")
+    private int mAliveNamespacesCount;
+
+    @Constructor
+    StorageInfo(
+            @Param(id = 1) long sizeBytes,
+            @Param(id = 2) int aliveDocumentsCount,
+            @Param(id = 3) int aliveNamespacesCount) {
+        mSizeBytes = sizeBytes;
+        mAliveDocumentsCount = aliveDocumentsCount;
+        mAliveNamespacesCount = aliveNamespacesCount;
     }
 
     /** Returns the estimated size of the session's database in bytes. */
     public long getSizeBytes() {
-        return mBundle.getLong(SIZE_BYTES_FIELD);
+        return mSizeBytes;
     }
 
     /**
@@ -58,7 +68,7 @@
      * set in {@link GenericDocument.Builder#setTtlMillis}.
      */
     public int getAliveDocumentsCount() {
-        return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
+        return mAliveDocumentsCount;
     }
 
     /**
@@ -69,7 +79,7 @@
      * set in {@link GenericDocument.Builder#setTtlMillis}.
      */
     public int getAliveNamespacesCount() {
-        return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
+        return mAliveNamespacesCount;
     }
 
     /** Builder for {@link StorageInfo} objects. */
@@ -105,11 +115,14 @@
         /** Builds a {@link StorageInfo} object. */
         @NonNull
         public StorageInfo build() {
-            Bundle bundle = new Bundle();
-            bundle.putLong(SIZE_BYTES_FIELD, mSizeBytes);
-            bundle.putInt(ALIVE_DOCUMENTS_COUNT, mAliveDocumentsCount);
-            bundle.putInt(ALIVE_NAMESPACES_COUNT, mAliveNamespacesCount);
-            return new StorageInfo(bundle);
+            return new StorageInfo(mSizeBytes, mAliveDocumentsCount, mAliveNamespacesCount);
         }
     }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        StorageInfoCreator.writeToParcel(this, dest, flags);
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
deleted file mode 100644
index 2bea91f..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
+++ /dev/null
@@ -1,446 +0,0 @@
-/*
- * 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.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.safeparcel.AbstractSafeParcelable;
-import androidx.appsearch.safeparcel.SafeParcelable;
-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityDocumentCreator;
-import androidx.collection.ArraySet;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * Holds the visibility settings that apply to a schema type.
- * @exportToFramework:hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityDocumentCreator")
-public final class VisibilityDocument extends AbstractSafeParcelable {
-    @NonNull
-    public static final VisibilityDocumentCreator CREATOR = new VisibilityDocumentCreator();
-
-    /**
-     * The Schema type for documents that hold AppSearch's metadata, such as 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();
-
-    @NonNull
-    @Field(id = 1, getter = "getId")
-    private String mId;
-
-    @Field(id = 2, getter = "isNotDisplayedBySystem")
-    private final boolean mIsNotDisplayedBySystem;
-
-    @NonNull
-    @Field(id = 3, getter = "getPackageNames")
-    private final String[] mPackageNames;
-
-    @NonNull
-    @Field(id = 4, getter = "getSha256Certs")
-    private final byte[][] mSha256Certs;
-
-    @Nullable
-    @Field(id = 5, getter = "getPermissionDocuments")
-    private final VisibilityPermissionDocument[] mPermissionDocuments;
-
-    @Nullable
-    // We still need to convert this class to a GenericDocument until we completely treat it
-    // differently in AppSearchImpl.
-    // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
-    //  visibility information.
-    private GenericDocument mGenericDocument;
-
-    @Nullable
-    private Integer mHashCode;
-
-    @Constructor
-    VisibilityDocument(
-            @Param(id = 1) @NonNull String id,
-            @Param(id = 2) boolean isNotDisplayedBySystem,
-            @Param(id = 3) @NonNull String[] packageNames,
-            @Param(id = 4) @NonNull byte[][] sha256Certs,
-            @Param(id = 5) @Nullable VisibilityPermissionDocument[] permissionDocuments) {
-        mId = Objects.requireNonNull(id);
-        mIsNotDisplayedBySystem = isNotDisplayedBySystem;
-        mPackageNames = Objects.requireNonNull(packageNames);
-        mSha256Certs = Objects.requireNonNull(sha256Certs);
-        mPermissionDocuments = permissionDocuments;
-    }
-
-    /**
-     * Gets the id for this VisibilityDocument.
-     *
-     * <p>This is being used as the document id when we convert a {@link VisibilityDocument}
-     * to a {@link GenericDocument}.
-     */
-    @NonNull
-    public String getId() {
-        return mId;
-    }
-
-    /** Returns whether this schema is visible to the system. */
-    public boolean isNotDisplayedBySystem() {
-        return mIsNotDisplayedBySystem;
-    }
-
-    /**
-     * 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 mPackageNames;
-    }
-
-    /**
-     * 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 mSha256Certs;
-    }
-
-    /** Gets a list of {@link VisibilityDocument}.
-     *
-     * <p>A {@link VisibilityDocument} holds all required permissions for the caller need to have
-     * to access the schema this {@link VisibilityDocument} presents.
-     */
-    @Nullable
-    VisibilityPermissionDocument[] getPermissionDocuments() {
-        return mPermissionDocuments;
-    }
-
-    /**
-     * Returns an array of Android Permissions that caller mush hold to access the schema this
-     * {@link VisibilityDocument} represents.
-     */
-    @NonNull
-    public Set<Set<Integer>> getVisibleToPermissions() {
-        if (mPermissionDocuments == null) {
-            return Collections.emptySet();
-        }
-        Set<Set<Integer>> visibleToPermissions = new ArraySet<>(mPermissionDocuments.length);
-        for (VisibilityPermissionDocument permissionDocument : mPermissionDocuments) {
-            Set<Integer> requiredPermissions = permissionDocument.getAllRequiredPermissions();
-            if (requiredPermissions != null) {
-                visibleToPermissions.add(requiredPermissions);
-            }
-        }
-        return visibleToPermissions;
-    }
-
-    /** 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;
-    }
-
-    /**
-     * Generates a {@link GenericDocument} from the current class.
-     *
-     * <p>This conversion is needed until we don't treat Visibility related documents as
-     * {@link GenericDocument}s internally.
-     */
-    @NonNull
-    public GenericDocument toGenericDocument() {
-        if (mGenericDocument == null) {
-            GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
-                    NAMESPACE, mId, SCHEMA_TYPE);
-            builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, mIsNotDisplayedBySystem);
-            builder.setPropertyString(PACKAGE_NAME_PROPERTY, mPackageNames);
-            builder.setPropertyBytes(SHA_256_CERT_PROPERTY, mSha256Certs);
-
-            // Generate an array of GenericDocument for VisibilityPermissionDocument.
-            if (mPermissionDocuments != null) {
-                GenericDocument[] permissionGenericDocs =
-                        new GenericDocument[mPermissionDocuments.length];
-                for (int i = 0; i < mPermissionDocuments.length; ++i) {
-                    permissionGenericDocs[i] = mPermissionDocuments[i].toGenericDocument();
-                }
-                builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs);
-            }
-
-            // The creationTimestamp doesn't matter for Visibility documents.
-            // But to make tests pass, we set it 0 so two GenericDocuments generated from
-            // the same VisibilityDocument can be same.
-            builder.setCreationTimestampMillis(0L);
-
-            mGenericDocument = builder.build();
-        }
-        return mGenericDocument;
-    }
-
-    @Override
-    public int hashCode() {
-        if (mHashCode == null) {
-            mHashCode = Objects.hash(
-                    mId,
-                    mIsNotDisplayedBySystem,
-                    Arrays.hashCode(mPackageNames),
-                    Arrays.deepHashCode(mSha256Certs),
-                    Arrays.hashCode(mPermissionDocuments));
-        }
-        return mHashCode;
-    }
-
-    @Override
-    public boolean equals(@Nullable Object other) {
-        if (this == other) {
-            return true;
-        }
-        if (!(other instanceof VisibilityDocument)) {
-            return false;
-        }
-        VisibilityDocument otherVisibilityDocument = (VisibilityDocument) other;
-        return mId.equals(otherVisibilityDocument.mId)
-                && mIsNotDisplayedBySystem == otherVisibilityDocument.mIsNotDisplayedBySystem
-                && Arrays.equals(
-                mPackageNames, otherVisibilityDocument.mPackageNames)
-                && Arrays.deepEquals(
-                mSha256Certs, otherVisibilityDocument.mSha256Certs)
-                && Arrays.equals(
-                mPermissionDocuments, otherVisibilityDocument.mPermissionDocuments);
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        VisibilityDocumentCreator.writeToParcel(this, dest, flags);
-    }
-
-    /** Builder for {@link VisibilityDocument}. */
-    public static final class Builder {
-        private final Set<PackageIdentifier> mPackageIdentifiers = new ArraySet<>();
-        private String mId;
-        private boolean mIsNotDisplayedBySystem;
-        private VisibilityPermissionDocument[] mPermissionDocuments;
-
-        /**
-         * 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) {
-            mId = Objects.requireNonNull(id);
-        }
-
-        /**
-         * Constructs a {@link VisibilityDocument} from a {@link GenericDocument}.
-         *
-         * <p>This constructor is still needed until we don't treat Visibility related documents as
-         * {@link GenericDocument}s internally.
-         */
-        public Builder(@NonNull GenericDocument genericDocument) {
-            Objects.requireNonNull(genericDocument);
-
-            mId = genericDocument.getId();
-            mIsNotDisplayedBySystem = genericDocument.getPropertyBoolean(
-                    NOT_DISPLAYED_BY_SYSTEM_PROPERTY);
-
-            String[] packageNames = genericDocument.getPropertyStringArray(PACKAGE_NAME_PROPERTY);
-            byte[][] sha256Certs = genericDocument.getPropertyBytesArray(SHA_256_CERT_PROPERTY);
-            for (int i = 0; i < packageNames.length; ++i) {
-                mPackageIdentifiers.add(new PackageIdentifier(packageNames[i], sha256Certs[i]));
-            }
-
-            GenericDocument[] permissionDocs =
-                    genericDocument.getPropertyDocumentArray(PERMISSION_PROPERTY);
-            if (permissionDocs != null) {
-                mPermissionDocuments = new VisibilityPermissionDocument[permissionDocs.length];
-                for (int i = 0; i < permissionDocs.length; ++i) {
-                    mPermissionDocuments[i] = new VisibilityPermissionDocument.Builder(
-                            permissionDocs[i]).build();
-                }
-            }
-        }
-
-        public Builder(@NonNull VisibilityDocument visibilityDocument) {
-            Objects.requireNonNull(visibilityDocument);
-
-            mIsNotDisplayedBySystem = visibilityDocument.mIsNotDisplayedBySystem;
-            mPermissionDocuments = visibilityDocument.mPermissionDocuments;
-            for (int i = 0; i < visibilityDocument.mPackageNames.length; ++i) {
-                mPackageIdentifiers.add(new PackageIdentifier(visibilityDocument.mPackageNames[i],
-                            visibilityDocument.mSha256Certs[i]));
-            }
-        }
-
-        /** Sets id. */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder setId(@NonNull String id) {
-            mId = Objects.requireNonNull(id);
-            return this;
-        }
-
-        /** Sets whether this schema has opted out of platform surfacing. */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
-            mIsNotDisplayedBySystem = notDisplayedBySystem;
-            return this;
-        }
-
-        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder addVisibleToPackages(@NonNull Set<PackageIdentifier> packageIdentifiers) {
-            mPackageIdentifiers.addAll(Objects.requireNonNull(packageIdentifiers));
-            return this;
-        }
-
-        /** Add {@link PackageIdentifier} of packages which has access to this schema. */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
-            mPackageIdentifiers.add(Objects.requireNonNull(packageIdentifier));
-            return this;
-        }
-
-        /**
-         * Sets 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.
-         */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder setVisibleToPermissions(@NonNull Set<Set<Integer>> visibleToPermissions) {
-            Objects.requireNonNull(visibleToPermissions);
-            mPermissionDocuments =
-                    new VisibilityPermissionDocument[visibleToPermissions.size()];
-            int i = 0;
-            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
-                mPermissionDocuments[i++] =
-                        new VisibilityPermissionDocument.Builder(
-                                NAMESPACE, /*id=*/ String.valueOf(i))
-                                .setVisibleToAllRequiredPermissions(allRequiredPermissions)
-                                .build();
-            }
-            return this;
-        }
-
-        /** Build a {@link VisibilityDocument} */
-        @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;
-            }
-            return new VisibilityDocument(mId, mIsNotDisplayedBySystem,
-                    packageNames, sha256Certs, mPermissionDocuments);
-        }
-    }
-}
-
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
new file mode 100644
index 0000000..40e0f81
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionConfigCreator;
+import androidx.collection.ArraySet;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The config class that holds all required permissions for a caller need to hold to access the
+ * schema which the outer {@link SchemaVisibilityConfig} represents.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityPermissionConfigCreator")
+public final class VisibilityPermissionConfig extends AbstractSafeParcelable {
+    @NonNull
+    public static final Parcelable.Creator<VisibilityPermissionConfig> CREATOR =
+            new VisibilityPermissionConfigCreator();
+
+    /**
+     * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
+     */
+    public static final String SCHEMA_TYPE = "VisibilityPermissionType";
+
+    /** Property that holds the required permissions to access the schema. */
+    public static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * <p>NOTE: If you update this, also update schema version number in
+     * VisibilityToDocumentConverter
+     */
+    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();
+
+    @Nullable
+    @Field(id = 1)
+    final int[] mAllRequiredPermissions;
+
+    @Nullable
+    // We still need to convert this class to a GenericDocument until we completely treat it
+    // differently in AppSearchImpl.
+    // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
+    //  visibility information.
+    private GenericDocument mGenericDocument;
+
+    @Nullable
+    private Integer mHashCode;
+
+    @Constructor
+    VisibilityPermissionConfig(@Param(id = 1) @Nullable int[] allRequiredPermissions) {
+        mAllRequiredPermissions = allRequiredPermissions;
+    }
+
+    /**
+     * Sets a set of Android Permissions that caller must hold to access the schema that the
+     * outer {@link SchemaVisibilityConfig} represents.
+     */
+    public VisibilityPermissionConfig(@NonNull Set<Integer> allRequiredPermissions) {
+        mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));
+    }
+
+    /**
+     * Returns an array of Android Permissions that caller mush hold to access the schema that the
+     * outer {@link SchemaVisibilityConfig} represents.
+     */
+    @Nullable
+    public Set<Integer> getAllRequiredPermissions() {
+        return toIntegerSet(mAllRequiredPermissions);
+    }
+
+    @NonNull
+    private static int[] toInts(@NonNull Set<Integer> properties) {
+        int[] outputs = new int[properties.size()];
+        int i = 0;
+        for (int property : properties) {
+            outputs[i++] = property;
+        }
+        return outputs;
+    }
+
+    @Nullable
+    private static Set<Integer> toIntegerSet(@Nullable int[] properties) {
+        if (properties == null) {
+            return null;
+        }
+        Set<Integer> outputs = new ArraySet<>(properties.length);
+        for (int property : properties) {
+            outputs.add(property);
+        }
+        return outputs;
+    }
+
+    /**
+     * Generates a {@link GenericDocument} from the current class.
+     *
+     * <p>This conversion is needed until we don't treat Visibility related documents as
+     * {@link GenericDocument}s internally.
+     */
+    @NonNull
+    public GenericDocument toGenericDocument() {
+        if (mGenericDocument == null) {
+            // This is used as a nested document, we do not need a namespace or id.
+            GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
+                    /*namespace=*/"", /*id=*/"", SCHEMA_TYPE);
+
+            if (mAllRequiredPermissions != null) {
+                // GenericDocument only supports long, so int[] needs to be converted to
+                // long[] here.
+                long[] longs = new long[mAllRequiredPermissions.length];
+                for (int i = 0; i < mAllRequiredPermissions.length; ++i) {
+                    longs[i] = mAllRequiredPermissions[i];
+                }
+                builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);
+            }
+
+            // The creationTimestamp doesn't matter for Visibility documents.
+            // But to make tests pass, we set it 0 so two GenericDocuments generated from
+            // the same VisibilityPermissionConfig can be same.
+            builder.setCreationTimestampMillis(0L);
+
+            mGenericDocument = builder.build();
+        }
+        return mGenericDocument;
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = Arrays.hashCode(mAllRequiredPermissions);
+        }
+        return mHashCode;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof VisibilityPermissionConfig)) {
+            return false;
+        }
+        VisibilityPermissionConfig otherVisibilityPermissionConfig =
+                (VisibilityPermissionConfig) other;
+        return Arrays.equals(mAllRequiredPermissions,
+                otherVisibilityPermissionConfig.mAllRequiredPermissions);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        VisibilityPermissionConfigCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
deleted file mode 100644
index 54269fd..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * 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 android.os.Parcel;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.annotation.CanIgnoreReturnValue;
-import androidx.appsearch.safeparcel.AbstractSafeParcelable;
-import androidx.appsearch.safeparcel.SafeParcelable;
-import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionDocumentCreator;
-import androidx.collection.ArraySet;
-
-import java.util.Arrays;
-import java.util.Objects;
-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.
- * @exportToFramework:hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
[email protected](creator = "VisibilityPermissionDocumentCreator")
-public final class VisibilityPermissionDocument extends AbstractSafeParcelable {
-    @NonNull
-    public static final VisibilityPermissionDocumentCreator CREATOR =
-            new VisibilityPermissionDocumentCreator();
-
-    /**
-     * The Schema type for documents that hold AppSearch's metadata, such as 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();
-
-    @NonNull
-    @Field(id = 1, getter = "getId")
-    private final String mId;
-
-    @NonNull
-    @Field(id = 2, getter = "getNamespace")
-    private final String mNamespace;
-
-    @Nullable
-    @Field(id = 3, getter = "getAllRequiredPermissionsInts")
-    // SafeParcelable doesn't support Set<Integer>, so we have to convert it to int[].
-    private final int[] mAllRequiredPermissions;
-
-    @Nullable
-    // We still need to convert this class to a GenericDocument until we completely treat it
-    // differently in AppSearchImpl.
-    // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store
-    //  visibility information.
-    private GenericDocument mGenericDocument;
-
-    @Nullable
-    private Integer mHashCode;
-
-    @Constructor
-    VisibilityPermissionDocument(
-            @Param(id = 1) @NonNull String id,
-            @Param(id = 2) @NonNull String namespace,
-            @Param(id = 3) @Nullable int[] allRequiredPermissions) {
-        mId = Objects.requireNonNull(id);
-        mNamespace = Objects.requireNonNull(namespace);
-        mAllRequiredPermissions = allRequiredPermissions;
-    }
-
-    /**
-     * Gets the id for this {@link VisibilityPermissionDocument}.
-     *
-     * <p>This is being used as the document id when we convert a
-     * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.
-     */
-    @NonNull
-    public String getId() {
-        return mId;
-    }
-
-    /**
-     * Gets the namespace for this {@link VisibilityPermissionDocument}.
-     *
-     * <p>This is being used as the namespace when we convert a
-     * {@link VisibilityPermissionDocument} to a {@link GenericDocument}.
-     */
-    @NonNull
-    public String getNamespace() {
-        return mNamespace;
-    }
-
-    /** Gets the required Android Permissions in an int array. */
-    @Nullable
-    int[] getAllRequiredPermissionsInts() {
-        return mAllRequiredPermissions;
-    }
-
-    /**
-     * 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 toIntegerSet(mAllRequiredPermissions);
-    }
-
-    @NonNull
-    private static int[] toInts(@NonNull Set<Integer> properties) {
-        int[] outputs = new int[properties.size()];
-        int i = 0;
-        for (int property : properties) {
-            outputs[i++] = property;
-        }
-        return outputs;
-    }
-
-    @Nullable
-    private static Set<Integer> toIntegerSet(@Nullable int[] properties) {
-        if (properties == null) {
-            return null;
-        }
-        Set<Integer> outputs = new ArraySet<>(properties.length);
-        for (int property : properties) {
-            outputs.add(property);
-        }
-        return outputs;
-    }
-
-    /**
-     * Generates a {@link GenericDocument} from the current class.
-     *
-     * <p>This conversion is needed until we don't treat Visibility related documents as
-     * {@link GenericDocument}s internally.
-     */
-    @NonNull
-    public GenericDocument toGenericDocument() {
-        if (mGenericDocument == null) {
-            GenericDocument.Builder<?> builder = new GenericDocument.Builder<>(
-                    mNamespace, mId, SCHEMA_TYPE);
-
-            if (mAllRequiredPermissions != null) {
-                // GenericDocument only supports long, so int[] needs to be converted to
-                // long[] here.
-                long[] longs = new long[mAllRequiredPermissions.length];
-                for (int i = 0; i < mAllRequiredPermissions.length; ++i) {
-                    longs[i] = mAllRequiredPermissions[i];
-                }
-                builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs);
-            }
-
-            mGenericDocument = builder.build();
-        }
-        return mGenericDocument;
-    }
-
-    @Override
-    public int hashCode() {
-        if (mHashCode == null) {
-            mHashCode = Objects.hash(mId, mNamespace, Arrays.hashCode(mAllRequiredPermissions));
-        }
-        return mHashCode;
-    }
-
-    @Override
-    public boolean equals(@Nullable Object other) {
-        if (this == other) {
-            return true;
-        }
-        if (!(other instanceof VisibilityPermissionDocument)) {
-            return false;
-        }
-        VisibilityPermissionDocument otherVisibilityPermissionDocument =
-                (VisibilityPermissionDocument) other;
-        return mId.equals(otherVisibilityPermissionDocument.mId)
-                && mNamespace.equals(otherVisibilityPermissionDocument.mNamespace)
-                && Arrays.equals(
-                mAllRequiredPermissions,
-                otherVisibilityPermissionDocument.mAllRequiredPermissions);
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        VisibilityPermissionDocumentCreator.writeToParcel(this, dest, flags);
-    }
-
-    /** Builder for {@link VisibilityPermissionDocument}. */
-    public static final class Builder {
-        private String mId;
-        private String mNamespace;
-        private int[] mAllRequiredPermissions;
-
-        /**
-         * Constructs a {@link VisibilityPermissionDocument} from a {@link GenericDocument}.
-         *
-         * <p>This constructor is still needed until we don't treat Visibility related documents as
-         * {@link GenericDocument}s internally.
-         */
-        public Builder(@NonNull GenericDocument genericDocument) {
-            Objects.requireNonNull(genericDocument);
-            mId = genericDocument.getId();
-            mNamespace = genericDocument.getNamespace();
-            // GenericDocument only supports long[], so we need to convert it back to int[].
-            long[] longs = genericDocument.getPropertyLongArray(
-                    ALL_REQUIRED_PERMISSIONS_PROPERTY);
-            if (longs != null) {
-                mAllRequiredPermissions = new int[longs.length];
-                for (int i = 0; i < longs.length; ++i) {
-                    mAllRequiredPermissions[i] = (int) longs[i];
-                }
-            }
-        }
-
-        /** Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}. */
-        public Builder(@NonNull String namespace, @NonNull String id) {
-            mNamespace = Objects.requireNonNull(namespace);
-            mId = Objects.requireNonNull(id);
-        }
-
-        /**
-         * Sets a set of Android Permissions that caller mush hold to access the schema that the
-         * outer {@link VisibilityDocument} represents.
-         */
-        @CanIgnoreReturnValue
-        @NonNull
-        public Builder setVisibleToAllRequiredPermissions(
-                @NonNull Set<Integer> allRequiredPermissions) {
-            mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions));
-            return this;
-        }
-
-        /** Builds a {@link VisibilityPermissionDocument} */
-        @NonNull
-        public VisibilityPermissionDocument build() {
-            return new VisibilityPermissionDocument(mId,
-                    mNamespace,
-                    mAllRequiredPermissions);
-        }
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
new file mode 100644
index 0000000..4fb00e1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.checker.initialization.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.FIELD,
+        ElementType.LOCAL_VARIABLE,
+        ElementType.METHOD,
+        ElementType.PACKAGE,
+        ElementType.PARAMETER,
+        ElementType.TYPE,
+        ElementType.TYPE_PARAMETER,
+        ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnderInitialization {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
new file mode 100644
index 0000000..8faa0dd
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.checker.initialization.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.FIELD,
+        ElementType.LOCAL_VARIABLE,
+        ElementType.METHOD,
+        ElementType.PACKAGE,
+        ElementType.PARAMETER,
+        ElementType.TYPE,
+        ElementType.TYPE_PARAMETER,
+        ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnknownInitialization {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
new file mode 100644
index 0000000..c9137c5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.checker.nullness.qual;
+
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.FIELD,
+        ElementType.LOCAL_VARIABLE,
+        ElementType.METHOD,
+        ElementType.PACKAGE,
+        ElementType.PARAMETER,
+        ElementType.TYPE,
+        ElementType.TYPE_PARAMETER,
+        ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface Nullable {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
new file mode 100644
index 0000000..4c39b9b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.checker.nullness.qual;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/**
+ * <!--@exportToFramework:hide-->
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Target({
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.FIELD,
+        ElementType.LOCAL_VARIABLE,
+        ElementType.METHOD,
+        ElementType.PACKAGE,
+        ElementType.PARAMETER,
+        ElementType.TYPE,
+        ElementType.TYPE_PARAMETER,
+        ElementType.TYPE_USE})
+@Retention(RetentionPolicy.SOURCE)
+public @interface RequiresNonNull {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
index 2930d2d..b39435f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
@@ -27,7 +27,7 @@
  * for propagating to the client.
  */
 public class AppSearchException extends Exception {
-    private final @AppSearchResult.ResultCode int mResultCode;
+    @AppSearchResult.ResultCode private final int mResultCode;
 
     /**
      * Initializes an {@link AppSearchException} with no message.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
similarity index 60%
copy from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
copy to appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
index 5138d2d..693be73 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
@@ -14,27 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.pdf.pdflib;
+package androidx.appsearch.flags;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.pdf.models.PdfDocumentRemote;
 
 /**
+ * Indicates an API is part of a feature that is guarded by an aconfig flag in the framework, and
+ * only available if the flag is enabled.
  *
+ * <p>Our own Jetpack version is created here for code sync purpose.
  */
+// @exportToFramework:skipFile()
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentRemoteProto {
-    private PdfDocumentRemote mRemote;
-
-    public PdfDocumentRemoteProto(@NonNull PdfDocumentRemote remote) {
-        this.mRemote = remote;
-    }
-
-    @NonNull
-    public PdfDocumentRemote getPdfDocumentRemote() {
-        return mRemote;
-    }
-
-    // TODO: Add goto links methods from the original kotlin file
+public @interface FlaggedApi {
+    String value();
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
new file mode 100644
index 0000000..5ad11f3
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+
+import java.util.Collection;
+
+/**
+ * Flags to control different features.
+ *
+ * <p>In Jetpack, those values can't be changed during runtime.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Flags {
+    private Flags() {
+    }
+
+    // The prefix of all the flags defined for AppSearch. The prefix has
+    // "com.android.appsearch.flags", aka the package name for generated AppSearch flag classes in
+    // the framework, plus an additional trailing '.'.
+    private static final String FLAG_PREFIX =
+            "com.android.appsearch.flags.";
+
+    // The full string values for flags defined in the framework.
+    //
+    // The values of the static variables are the names of the flag defined in the framework's
+    // aconfig files. E.g. "enable_safe_parcelable", with FLAG_PREFIX as the prefix.
+    //
+    // The name of the each static variable should be "FLAG_" + capitalized value of the flag.
+
+    /** Enable SafeParcelable related features. */
+    public static final String FLAG_ENABLE_SAFE_PARCELABLE_2 =
+            FLAG_PREFIX + "enable_safe_parcelable_2";
+
+    /** Enable the "hasProperty" function in list filter query expressions. */
+    public static final String FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION =
+            FLAG_PREFIX + "enable_list_filter_has_property_function";
+
+    /** Enable the "tokenize" function in list filter query expressions. */
+    public static final String FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION =
+            FLAG_PREFIX + "enable_list_filter_tokenize_function";
+
+
+    /** Enable Schema Type Grouping related features. */
+    public static final String FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA =
+            FLAG_PREFIX + "enable_grouping_type_per_schema";
+
+    /** Enable GenericDocument to take another GenericDocument to copy construct. */
+    public static final String FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR =
+            FLAG_PREFIX + "enable_generic_document_copy_constructor";
+
+    /**
+     * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and
+     * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties}.
+     */
+    public static final String FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES =
+            FLAG_PREFIX + "enable_search_spec_filter_properties";
+    /**
+     * Enable the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} method.
+     */
+    public static final String FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG =
+            FLAG_PREFIX + "enable_search_spec_set_search_source_log_tag";
+
+    /** Enable addTakenActions API in PutDocumentsRequest. */
+    public static final String FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS =
+            FLAG_PREFIX + "enable_put_documents_request_add_taken_actions";
+
+    /** Enable setPubliclyVisibleSchema in SetSchemaRequest. */
+    public static final String FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA = FLAG_PREFIX
+            + "enable_set_publicly_visible_schema";
+
+    /**
+     * Enable {@link androidx.appsearch.app.GenericDocument.Builder} to use previously hidden
+     * methods.
+     */
+    public static final String FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS = FLAG_PREFIX
+            + "enable_generic_document_builder_hidden_methods";
+
+    public static final String FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS = FLAG_PREFIX
+            + "enable_set_schema_visible_to_configs";
+
+    /** Enable {@link androidx.appsearch.app.EnterpriseGlobalSearchSession}. */
+    public static final String FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION =
+            FLAG_PREFIX + "enable_enterprise_global_search_session";
+
+    /**
+     * Enables {@link android.app.appsearch.functions.AppFunctionManager} and app functions related
+     * stuff.
+     */
+    public static final String FLAG_ENABLE_APP_FUNCTIONS = FLAG_PREFIX + "enable_app_functions";
+
+    /**
+     * Enable {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and
+     * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} which were previously
+     * hidden.
+     */
+    public static final String FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED =
+            FLAG_PREFIX + "enable_result_denied_and_result_rate_limited";
+
+    /**
+     * Enables {@link AppSearchSchema#getParentTypes()},
+     * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of
+     * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}.
+     */
+    public static final String FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES =
+            FLAG_PREFIX + "enable_get_parent_types_and_indexable_nested_properties";
+
+    /** Enables embedding search related APIs. */
+    public static final String FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG =
+            FLAG_PREFIX + "enable_schema_embedding_property_config";
+
+    /** Enables informational ranking expressions. */
+    public static final String FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS =
+            FLAG_PREFIX + "enable_informational_ranking_expressions";
+
+    // Whether the features should be enabled.
+    //
+    // In Jetpack, those should always return true.
+
+    /** Whether SafeParcelable should be enabled. */
+    public static boolean enableSafeParcelable() {
+        return true;
+    }
+
+    /** Whether the "hasProperty" function in list filter query expressions should be enabled. */
+    public static boolean enableListFilterHasPropertyFunction() {
+        return true;
+    }
+
+    /** Whether Schema Type Grouping should be enabled. */
+    public static boolean enableGroupingTypePerSchema() {
+        return true;
+    }
+
+    /** Whether Generic Document Copy Constructing should be enabled. */
+    public static boolean enableGenericDocumentCopyConstructor() {
+        return true;
+    }
+
+    /**
+     * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and
+     * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties} should be
+     * enabled.
+     */
+    public static boolean enableSearchSpecFilterProperties() {
+        return true;
+    }
+
+    /**
+     * Whether the {@link androidx.appsearch.app.SearchSpec.Builder#setSearchSourceLogTag} should
+     * be enabled.
+     */
+    public static boolean enableSearchSpecSetSearchSourceLogTag() {
+        return true;
+    }
+
+    /** Whether addTakenActions API in PutDocumentsRequest should be enabled. */
+    public static boolean enablePutDocumentsRequestAddTakenActions() {
+        return true;
+    }
+
+    /** Whether setPubliclyVisibleSchema in SetSchemaRequest.Builder should be enabled. */
+    public static boolean enableSetPubliclyVisibleSchema() {
+        return true;
+    }
+
+    /**
+     * Whether {@link androidx.appsearch.app.GenericDocument.Builder#setNamespace(String)},
+     * {@link androidx.appsearch.app.GenericDocument.Builder#setId(String)},
+     * {@link androidx.appsearch.app.GenericDocument.Builder#setSchemaType(String)}, and
+     * {@link androidx.appsearch.app.GenericDocument.Builder#clearProperty(String)}
+     * should be enabled.
+     */
+    public static boolean enableGenericDocumentBuilderHiddenMethods() {
+        return true;
+    }
+
+    /**
+     * Whether
+     * {@link androidx.appsearch.app.SetSchemaRequest.Builder #setSchemaTypeVisibilityForConfigs}
+     * should be enabled.
+     */
+    public static boolean enableSetSchemaVisibleToConfigs() {
+        return true;
+    }
+
+    /** Whether {@link androidx.appsearch.app.EnterpriseGlobalSearchSession} should be enabled. */
+    public static boolean enableEnterpriseGlobalSearchSession() {
+        return true;
+    }
+
+    /**
+     * Whether {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and
+     * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} should be enabled.
+     */
+    public static boolean enableResultDeniedAndResultRateLimited() {
+        return true;
+    }
+
+    /**
+     * Whether {@link AppSearchSchema#getParentTypes()},
+     * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of
+     * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}
+     * should be enabled.
+     */
+    public static boolean enableGetParentTypesAndIndexableNestedProperties() {
+        return true;
+    }
+
+    /** Whether embedding search related APIs should be enabled. */
+    public static boolean enableSchemaEmbeddingPropertyConfig() {
+        return true;
+    }
+
+    /** Whether the "tokenize" function in list filter query expressions should be enabled. */
+    public static boolean enableListFilterTokenizeFunction() {
+        return true;
+    }
+
+    /** Whether informational ranking expressions should be enabled. */
+    public static boolean enableInformationalRankingExpressions() {
+        return true;
+    }
+}
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 903929b..baae8d1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
@@ -17,7 +17,8 @@
 package androidx.appsearch.observer;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -27,6 +28,11 @@
 import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.DocumentClassFactoryRegistry;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.ObserverSpecCreator;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -41,31 +47,27 @@
  * 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";
[email protected](creator = "ObserverSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class ObserverSpec extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<ObserverSpec> CREATOR =
+            new ObserverSpecCreator();
 
-    private final Bundle mBundle;
+    @Field(id = 1)
+    final List<String> mFilterSchemas;
 
     /** Populated on first use */
-    @Nullable
-    private volatile Set<String> mFilterSchemas;
+    @Nullable private volatile Set<String> mFilterSchemasCached;
 
     /** @exportToFramework:hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public ObserverSpec(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-    }
-
-    /**
-     * Returns the {@link Bundle} backing this spec.
-     *
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    @Constructor
+    public ObserverSpec(
+            @Param(id = 1) @NonNull List<String> filterSchemas) {
+        mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
     }
 
     /**
@@ -75,15 +77,14 @@
      */
     @NonNull
     public Set<String> getFilterSchemas() {
-        if (mFilterSchemas == null) {
-            List<String> schemas = mBundle.getStringArrayList(FILTER_SCHEMA_FIELD);
-            if (schemas == null) {
-                mFilterSchemas = Collections.emptySet();
+        if (mFilterSchemasCached == null) {
+            if (mFilterSchemas == null) {
+                mFilterSchemasCached = Collections.emptySet();
             } else {
-                mFilterSchemas = Collections.unmodifiableSet(new ArraySet<>(schemas));
+                mFilterSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mFilterSchemas));
             }
         }
-        return mFilterSchemas;
+        return mFilterSchemasCached;
     }
 
     /** Builder for {@link ObserverSpec} instances. */
@@ -134,7 +135,7 @@
         @SuppressLint("MissingGetterMatchingBuilder")
         @CanIgnoreReturnValue
         @NonNull
-        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
+        public Builder addFilterDocumentClasses(@NonNull java.lang.Class<?>... documentClasses)
                 throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
@@ -155,12 +156,13 @@
         @CanIgnoreReturnValue
         @NonNull
         public Builder addFilterDocumentClasses(
-                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+                @NonNull Collection<? extends java.lang.Class<?>> documentClasses)
+                throws AppSearchException {
             Preconditions.checkNotNull(documentClasses);
             resetIfBuilt();
             List<String> schemas = new ArrayList<>(documentClasses.size());
             DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
-            for (Class<?> documentClass : documentClasses) {
+            for (java.lang.Class<?> documentClass : documentClasses) {
                 DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                 schemas.add(factory.getSchemaName());
             }
@@ -172,10 +174,8 @@
         /** Constructs a new {@link ObserverSpec} from the contents of this builder. */
         @NonNull
         public ObserverSpec build() {
-            Bundle bundle = new Bundle();
-            bundle.putStringArrayList(FILTER_SCHEMA_FIELD, mFilterSchemas);
             mBuilt = true;
-            return new ObserverSpec(bundle);
+            return new ObserverSpec(mFilterSchemas);
         }
 
         private void resetIfBuilt() {
@@ -185,4 +185,11 @@
             }
         }
     }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ObserverSpecCreator.writeToParcel(this, dest, flags);
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
index a677ff8..bdc4bb5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
@@ -211,4 +211,10 @@
      */
     public void writeToParcel(@NonNull Parcel dest, int flags) {
     }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public final int describeContents() {
+        return 0;
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
index d921290..1d407fa 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
@@ -16,20 +16,22 @@
 
 package androidx.appsearch.safeparcel;
 
+import android.annotation.SuppressLint;
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator;
 import androidx.collection.ArrayMap;
 
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -41,9 +43,11 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
-public final class GenericDocumentParcel extends AbstractSafeParcelable {
+// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
+@SuppressLint("BanParcelableUsage")
+public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
     @NonNull
-    public static final GenericDocumentParcelCreator CREATOR =
+    public static final Parcelable.Creator<GenericDocumentParcel> CREATOR =
             new GenericDocumentParcelCreator();
 
     /** The default score of document. */
@@ -83,7 +87,15 @@
      */
     @Field(id = 7, getter = "getProperties")
     @NonNull
-    private final PropertyParcel[] mProperties;
+    private final List<PropertyParcel> mProperties;
+
+    /**
+     * Contains all parent properties for this {@link GenericDocument} in a list.
+     *
+     */
+    @Field(id = 8, getter = "getParentTypes")
+    @Nullable
+    private final List<String> mParentTypes;
 
     /**
      * Contains all properties in {@link GenericDocument} to support getting properties via name
@@ -110,9 +122,10 @@
             @Param(id = 4) long creationTimestampMillis,
             @Param(id = 5) long ttlMillis,
             @Param(id = 6) int score,
-            @Param(id = 7) @NonNull PropertyParcel[] properties) {
+            @Param(id = 7) @NonNull List<PropertyParcel> properties,
+            @Param(id = 8) @Nullable List<String> parentTypes) {
         this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
-                properties, createPropertyMapFromPropertyArray(properties));
+                properties, createPropertyMapFromPropertyArray(properties), parentTypes);
     }
 
     /**
@@ -128,8 +141,9 @@
             long creationTimestampMillis,
             long ttlMillis,
             int score,
-            @NonNull PropertyParcel[] properties,
-            @NonNull Map<String, PropertyParcel> propertyMap) {
+            @NonNull List<PropertyParcel> properties,
+            @NonNull Map<String, PropertyParcel> propertyMap,
+            @Nullable List<String> parentTypes) {
         mNamespace = Objects.requireNonNull(namespace);
         mId = Objects.requireNonNull(id);
         mSchemaType = Objects.requireNonNull(schemaType);
@@ -138,14 +152,23 @@
         mScore = score;
         mProperties = Objects.requireNonNull(properties);
         mPropertyMap = Objects.requireNonNull(propertyMap);
+        mParentTypes = parentTypes;
+    }
+
+    /** Returns the {@link GenericDocumentParcel} object from the given {@link GenericDocument}. */
+    @NonNull
+    public static GenericDocumentParcel fromGenericDocument(
+            @NonNull GenericDocument genericDocument) {
+        Objects.requireNonNull(genericDocument);
+        return genericDocument.getDocumentParcel();
     }
 
     private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
-            @NonNull PropertyParcel[] properties) {
+            @NonNull List<PropertyParcel> properties) {
         Objects.requireNonNull(properties);
-        Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.length);
-        for (int i = 0; i < properties.length; ++i) {
-            PropertyParcel property = properties[i];
+        Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.size());
+        for (int i = 0; i < properties.size(); ++i) {
+            PropertyParcel property = properties.get(i);
             propertyMap.put(property.getPropertyName(), property);
         }
         return propertyMap;
@@ -193,7 +216,7 @@
 
     /** Returns all the properties the document has. */
     @NonNull
-    public PropertyParcel[] getProperties() {
+    public List<PropertyParcel> getProperties() {
         return mProperties;
     }
 
@@ -203,6 +226,12 @@
         return mPropertyMap;
     }
 
+    /** Returns the list of parent types for the {@link GenericDocument}. */
+    @Nullable
+    public List<String> getParentTypes() {
+        return mParentTypes;
+    }
+
     @Override
     public boolean equals(@Nullable Object other) {
         if (this == other) {
@@ -218,8 +247,9 @@
                 && mTtlMillis == otherDocument.mTtlMillis
                 && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
                 && mScore == otherDocument.mScore
-                && Arrays.equals(mProperties, otherDocument.mProperties)
-                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);
+                && Objects.equals(mProperties, otherDocument.mProperties)
+                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)
+                && Objects.equals(mParentTypes, otherDocument.mParentTypes);
     }
 
     @Override
@@ -232,8 +262,9 @@
                     mTtlMillis,
                     mScore,
                     mCreationTimestampMillis,
-                    Arrays.hashCode(mProperties),
-                    mPropertyMap.hashCode());
+                    Objects.hashCode(mProperties),
+                    Objects.hashCode(mPropertyMap),
+                    Objects.hashCode(mParentTypes));
         }
         return mHashCode;
     }
@@ -252,10 +283,10 @@
         private long mTtlMillis;
         private int mScore;
         private Map<String, PropertyParcel> mPropertyMap;
-        private boolean mBuilt = false;
+        @Nullable private List<String> mParentTypes;
 
         /**
-         * Creates a new {@link GenericDocument.Builder}.
+         * Creates a new {@link GenericDocumentParcel.Builder}.
          *
          * <p>Document IDs are unique within a namespace.
          *
@@ -275,7 +306,6 @@
          * Creates a new {@link GenericDocumentParcel.Builder} from the given
          * {@link GenericDocumentParcel}.
          */
-        @VisibleForTesting
         public Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
             Objects.requireNonNull(documentSafeParcel);
 
@@ -292,6 +322,10 @@
             for (PropertyParcel value : propertyMap.values()) {
                 mPropertyMap.put(value.getPropertyName(), value);
             }
+
+            // We don't need to create a shallow copy here, as in the setter for ParentTypes we
+            // will create a new list anyway.
+            mParentTypes = documentSafeParcel.mParentTypes;
         }
 
         /**
@@ -306,7 +340,6 @@
         @NonNull
         public Builder setNamespace(@NonNull String namespace) {
             Objects.requireNonNull(namespace);
-            resetIfBuilt();
             mNamespace = namespace;
             return this;
         }
@@ -321,7 +354,6 @@
         @NonNull
         public Builder setId(@NonNull String id) {
             Objects.requireNonNull(id);
-            resetIfBuilt();
             mId = id;
             return this;
         }
@@ -336,7 +368,6 @@
         @NonNull
         public Builder setSchemaType(@NonNull String schemaType) {
             Objects.requireNonNull(schemaType);
-            resetIfBuilt();
             mSchemaType = schemaType;
             return this;
         }
@@ -345,7 +376,6 @@
         @CanIgnoreReturnValue
         @NonNull
         public Builder setScore(int score) {
-            resetIfBuilt();
             mScore = score;
             return this;
         }
@@ -364,7 +394,6 @@
         @NonNull
         public Builder setCreationTimestampMillis(
                 /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
-            resetIfBuilt();
             mCreationTimestampMillis = creationTimestampMillis;
             return this;
         }
@@ -388,12 +417,24 @@
             if (ttlMillis < 0) {
                 throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
             }
-            resetIfBuilt();
             mTtlMillis = ttlMillis;
             return this;
         }
 
         /**
+         * Sets the list of parent types of the {@link GenericDocument}'s type.
+         *
+         * <p>Child types must appear before parent types in the list.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setParentTypes(@NonNull List<String> parentTypes) {
+            Objects.requireNonNull(parentTypes);
+            mParentTypes = new ArrayList<>(parentTypes);
+            return this;
+        }
+
+        /**
          * Clears the value for the property with the given name.
          *
          * <p>Note that this method does not support property paths.
@@ -404,40 +445,43 @@
         @NonNull
         public Builder clearProperty(@NonNull String name) {
             Objects.requireNonNull(name);
-            resetIfBuilt();
             mPropertyMap.remove(name);
             return this;
         }
 
         /** puts an array of {@link String} in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
                 throws IllegalArgumentException {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setStringValues(values).build());
             return this;
         }
 
         /** puts an array of boolean in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setBooleanValues(values).build());
             return this;
         }
 
         /** puts an array of double in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setDoubleValues(values).build());
             return this;
         }
 
         /** puts an array of long in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setLongValues(values).build());
             return this;
         }
@@ -445,26 +489,47 @@
         /**
          * Converts and saves a byte[][] into {@link #mProperties}.
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setBytesValues(values).build());
             return this;
         }
 
         /** puts an array of {@link GenericDocumentParcel} in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name,
                 @NonNull GenericDocumentParcel[] values) {
-            mPropertyMap.put(name,
+            putInPropertyMap(name,
                     new PropertyParcel.Builder(name).setDocumentValues(values).build());
             return this;
         }
 
+        /** puts an array of {@link EmbeddingVector} in property map. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name,
+                @NonNull EmbeddingVector[] values) {
+            putInPropertyMap(name,
+                    new PropertyParcel.Builder(name).setEmbeddingValues(values).build());
+            return this;
+        }
+
+        /** Directly puts a {@link PropertyParcel} in property map. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name,
+                @NonNull PropertyParcel value) {
+            Objects.requireNonNull(value);
+            mPropertyMap.put(name, value);
+            return this;
+        }
+
         /** Builds the {@link GenericDocument} object. */
         @NonNull
         public GenericDocumentParcel build() {
-            mBuilt = true;
             // Set current timestamp for creation timestamp by default.
             if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
                 mCreationTimestampMillis = System.currentTimeMillis();
@@ -476,19 +541,8 @@
                     mCreationTimestampMillis,
                     mTtlMillis,
                     mScore,
-                    mPropertyMap.values().toArray(new PropertyParcel[0]));
-        }
-
-        void resetIfBuilt() {
-            if (mBuilt) {
-                Map<String, PropertyParcel> propertyMap = mPropertyMap;
-                mPropertyMap = new ArrayMap<>(propertyMap.size());
-                for (PropertyParcel value : propertyMap.values()) {
-                    // PropertyParcel is not deep copied since it is not mutable.
-                    mPropertyMap.put(value.getPropertyName(), value);
-                }
-                mBuilt = false;
-            }
+                    new ArrayList<>(mPropertyMap.values()),
+                    mParentTypes);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
new file mode 100644
index 0000000..98aee5b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.safeparcel;
+
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implemented creator for {@link GenericDocumentParcel}.
+ *
+ * <p>In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},
+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class GenericDocumentParcelCreator implements
+        Parcelable.Creator<GenericDocumentParcel> {
+    private static final String PROPERTIES_FIELD = "properties";
+    private static final String SCHEMA_TYPE_FIELD = "schemaType";
+    private static final String ID_FIELD = "id";
+    private static final String SCORE_FIELD = "score";
+    private static final String TTL_MILLIS_FIELD = "ttlMillis";
+    private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
+    private static final String NAMESPACE_FIELD = "namespace";
+    private static final String PARENT_TYPES_FIELD = "parentTypes";
+
+    /** Creates a {@link GenericDocumentParcel} from a {@link Bundle}. */
+    @NonNull
+    private static GenericDocumentParcel createGenericDocumentParcelFromBundle(
+            @NonNull Bundle genericDocumentParcelBundle) {
+        // Get namespace, id, and schema type
+        String namespace = genericDocumentParcelBundle.getString(NAMESPACE_FIELD);
+        String id = genericDocumentParcelBundle.getString(ID_FIELD);
+        String schemaType = genericDocumentParcelBundle.getString(SCHEMA_TYPE_FIELD);
+
+        // Those three can NOT be null.
+        if (namespace == null || id == null || schemaType == null) {
+            throw new IllegalArgumentException("GenericDocumentParcel bundle doesn't have "
+                    + "namespace, id, or schemaType.");
+        }
+
+        GenericDocumentParcel.Builder builder = new GenericDocumentParcel.Builder(namespace,
+                id, schemaType);
+        List<String> parentTypes =
+                genericDocumentParcelBundle.getStringArrayList(PARENT_TYPES_FIELD);
+        if (parentTypes != null) {
+            builder.setParentTypes(parentTypes);
+        }
+        builder.setScore(genericDocumentParcelBundle.getInt(SCORE_FIELD));
+        builder.setCreationTimestampMillis(
+                genericDocumentParcelBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD));
+        builder.setTtlMillis(genericDocumentParcelBundle.getLong(TTL_MILLIS_FIELD));
+
+        // properties
+        Bundle propertyBundle = genericDocumentParcelBundle.getBundle(PROPERTIES_FIELD);
+        if (propertyBundle != null) {
+            for (String propertyName : propertyBundle.keySet()) {
+                // SuppressWarnings can be applied on a local variable, but not any
+                // single line of code.
+                @SuppressWarnings("deprecation")
+                PropertyParcel propertyParcel = propertyBundle.getParcelable(propertyName);
+                builder.putInPropertyMap(propertyName, propertyParcel);
+            }
+        }
+
+        return builder.build();
+    }
+
+    /** Creates a {@link Bundle} from a {@link GenericDocumentParcel}. */
+    @NonNull
+    private static Bundle createBundleFromGenericDocumentParcel(
+            @NonNull GenericDocumentParcel genericDocumentParcel) {
+        Bundle genericDocumentParcelBundle = new Bundle();
+
+        // Common fields
+        genericDocumentParcelBundle.putString(NAMESPACE_FIELD,
+                genericDocumentParcel.getNamespace());
+        genericDocumentParcelBundle.putString(ID_FIELD, genericDocumentParcel.getId());
+        genericDocumentParcelBundle.putString(SCHEMA_TYPE_FIELD,
+                genericDocumentParcel.getSchemaType());
+        genericDocumentParcelBundle.putStringArrayList(PARENT_TYPES_FIELD,
+                (ArrayList<String>) genericDocumentParcel.getParentTypes());
+        genericDocumentParcelBundle.putInt(SCORE_FIELD, genericDocumentParcel.getScore());
+        genericDocumentParcelBundle.putLong(CREATION_TIMESTAMP_MILLIS_FIELD,
+                genericDocumentParcel.getCreationTimestampMillis());
+        genericDocumentParcelBundle.putLong(TTL_MILLIS_FIELD,
+                genericDocumentParcel.getTtlMillis());
+
+        // Properties
+        Bundle properties = new Bundle();
+        List<PropertyParcel> propertyParcels = genericDocumentParcel.getProperties();
+        for (int i = 0; i < propertyParcels.size(); ++i) {
+            PropertyParcel propertyParcel = propertyParcels.get(i);
+            properties.putParcelable(propertyParcel.getPropertyName(), propertyParcel);
+        }
+        genericDocumentParcelBundle.putBundle(PROPERTIES_FIELD, properties);
+
+        return genericDocumentParcelBundle;
+    }
+
+    @Nullable
+    @Override
+    public GenericDocumentParcel createFromParcel(Parcel in) {
+        Bundle bundle = in.readBundle(getClass().getClassLoader());
+        return createGenericDocumentParcelFromBundle(bundle);
+    }
+
+    @Override
+    public GenericDocumentParcel[] newArray(int size) {
+        return new GenericDocumentParcel[size];
+    }
+
+    /** Writes a {@link GenericDocumentParcel} to a {@link Parcel}. */
+    public static void writeToParcel(@NonNull GenericDocumentParcel genericDocumentParcel,
+            @NonNull android.os.Parcel parcel, int flags) {
+        parcel.writeBundle(createBundleFromGenericDocumentParcel(genericDocumentParcel));
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
new file mode 100644
index 0000000..f405f48
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.safeparcel;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Holds data for a {@link PackageIdentifier}.
+ *
+ * TODO(b/275592563): This class is currently used in GetSchemaResponse as a bundle, and
+ * therefore needs to implement Parcelable directly. Reassess if this is still needed once
+ * VisibilityConfig becomes available, and if not we should switch to a SafeParcelable
+ * implementation instead.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
[email protected](creator = "PackageIdentifierParcelCreator")
+@SuppressLint("BanParcelableUsage")
+public final class PackageIdentifierParcel extends AbstractSafeParcelable implements Parcelable {
+    @NonNull
+    public static final Parcelable.Creator<PackageIdentifierParcel> CREATOR =
+            new PackageIdentifierParcelCreator();
+
+    @Field(id = 1, getter = "getPackageName")
+    private final String mPackageName;
+    @Field(id = 2, getter = "getSha256Certificate")
+    private final byte[] mSha256Certificate;
+
+    /**
+     * Creates a unique identifier for a package.
+     *
+     * @see PackageIdentifier
+     */
+    @Constructor
+    public PackageIdentifierParcel(@Param(id = 1) @NonNull String packageName,
+            @Param(id = 2) @NonNull byte[] sha256Certificate) {
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
+    }
+
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    @NonNull
+    public byte[] getSha256Certificate() {
+        return mSha256Certificate;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof PackageIdentifierParcel)) {
+            return false;
+        }
+        final PackageIdentifierParcel other = (PackageIdentifierParcel) obj;
+        return mPackageName.equals(other.mPackageName)
+                && Arrays.equals(mSha256Certificate, other.mSha256Certificate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPackageName, Arrays.hashCode(mSha256Certificate));
+    }
+
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        PackageIdentifierParcelCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
new file mode 100644
index 0000000..16fe177
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.safeparcel;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * An implemented creator for {@link PackageIdentifierParcel}.
+ *
+ * <p>In Jetpack, {@link androidx.appsearch.app.PackageIdentifier} is serialized in a bundle for
+ * {@link androidx.appsearch.app.GetSchemaResponse}, and therefore needs to implement a real
+ * {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PackageIdentifierParcelCreator implements Parcelable.Creator<PackageIdentifierParcel> {
+    private static final String PACKAGE_NAME_FIELD = "packageName";
+    private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
+
+    public PackageIdentifierParcelCreator() {
+    }
+
+    /**
+     * Creates a {@link PackageIdentifierParcel} from a {@link Bundle}
+     */
+    @NonNull
+    private static PackageIdentifierParcel createPackageIdentifierFromBundle(
+            @NonNull Bundle packageIdentifierBundle) {
+        Objects.requireNonNull(packageIdentifierBundle);
+        String packageName =
+                Preconditions.checkNotNull(packageIdentifierBundle.getString(PACKAGE_NAME_FIELD));
+        byte[] sha256Certificate =
+                Preconditions.checkNotNull(
+                        packageIdentifierBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
+
+        return new PackageIdentifierParcel(packageName, sha256Certificate);
+    }
+
+    /** Creates a {@link Bundle} from a {@link PackageIdentifierParcel}. */
+    @NonNull
+    private static Bundle createBundleFromPackageIdentifier(
+            @NonNull PackageIdentifierParcel packageIdentifierParcel) {
+        Objects.requireNonNull(packageIdentifierParcel);
+        Bundle packageIdentifierBundle = new Bundle();
+        packageIdentifierBundle.putString(PACKAGE_NAME_FIELD,
+                packageIdentifierParcel.getPackageName());
+        packageIdentifierBundle.putByteArray(SHA256_CERTIFICATE_FIELD,
+                packageIdentifierParcel.getSha256Certificate());
+
+        return packageIdentifierBundle;
+    }
+
+    @NonNull
+    @Override
+    public PackageIdentifierParcel createFromParcel(Parcel parcel) {
+        Bundle bundle = Preconditions.checkNotNull(parcel.readBundle(getClass().getClassLoader()));
+        return createPackageIdentifierFromBundle(bundle);
+    }
+
+    @NonNull
+    @Override
+    public PackageIdentifierParcel[] newArray(int size) {
+        return new PackageIdentifierParcel[size];
+    }
+
+    /** Writes a {@link PackageIdentifierParcel} to a {@link Parcel}. */
+    public static void writeToParcel(@NonNull PackageIdentifierParcel packageIdentifierParcel,
+            @NonNull android.os.Parcel parcel, int flags) {
+        parcel.writeBundle(createBundleFromPackageIdentifier(packageIdentifierParcel));
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
index 61029bd..96c53ae 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
@@ -17,6 +17,7 @@
 package androidx.appsearch.safeparcel;
 
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -27,10 +28,12 @@
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JoinableValueType;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TokenizerType;
 import androidx.appsearch.safeparcel.stub.StubCreators.DocumentIndexingConfigParcelCreator;
+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingIndexingConfigParcelCreator;
 import androidx.appsearch.safeparcel.stub.StubCreators.IntegerIndexingConfigParcelCreator;
 import androidx.appsearch.safeparcel.stub.StubCreators.JoinableConfigParcelCreator;
 import androidx.appsearch.safeparcel.stub.StubCreators.PropertyConfigParcelCreator;
 import androidx.appsearch.safeparcel.stub.StubCreators.StringIndexingConfigParcelCreator;
+import androidx.core.util.ObjectsCompat;
 
 import java.util.List;
 import java.util.Objects;
@@ -49,7 +52,8 @@
 @SafeParcelable.Class(creator = "PropertyConfigParcelCreator")
 public final class PropertyConfigParcel extends AbstractSafeParcelable {
     @NonNull
-    public static final PropertyConfigParcelCreator CREATOR = new PropertyConfigParcelCreator();
+    public static final Parcelable.Creator<PropertyConfigParcel> CREATOR =
+            new PropertyConfigParcelCreator();
 
     @Field(id = 1, getter = "getName")
     private final String mName;
@@ -63,23 +67,32 @@
     private final int mCardinality;
 
     @Field(id = 4, getter = "getSchemaType")
-    private final String mSchemaType;
+    @Nullable private final String mSchemaType;
 
     @Field(id = 5, getter = "getStringIndexingConfigParcel")
-    private final StringIndexingConfigParcel mStringIndexingConfigParcel;
+    @Nullable private final StringIndexingConfigParcel mStringIndexingConfigParcel;
 
     @Field(id = 6, getter = "getDocumentIndexingConfigParcel")
-    private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
+    @Nullable private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
 
     @Field(id = 7, getter = "getIntegerIndexingConfigParcel")
-    private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
+    @Nullable private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
 
     @Field(id = 8, getter = "getJoinableConfigParcel")
-    private final JoinableConfigParcel mJoinableConfigParcel;
+    @Nullable private final JoinableConfigParcel mJoinableConfigParcel;
+
+    @Field(id = 9, getter = "getDescription")
+    private final String mDescription;
+
+    @Field(id = 10, getter = "getEmbeddingIndexingConfigParcel")
+    private final EmbeddingIndexingConfigParcel mEmbeddingIndexingConfigParcel;
+
+    @Nullable
+    private Integer mHashCode;
 
     /** Constructor for {@link PropertyConfigParcel}. */
     @Constructor
-    public PropertyConfigParcel(
+    PropertyConfigParcel(
             @Param(id = 1) @NonNull String name,
             @Param(id = 2) @DataType int dataType,
             @Param(id = 3) @Cardinality int cardinality,
@@ -87,7 +100,9 @@
             @Param(id = 5) @Nullable StringIndexingConfigParcel stringIndexingConfigParcel,
             @Param(id = 6) @Nullable DocumentIndexingConfigParcel documentIndexingConfigParcel,
             @Param(id = 7) @Nullable IntegerIndexingConfigParcel integerIndexingConfigParcel,
-            @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel) {
+            @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel,
+            @Param(id = 9) @NonNull String description,
+            @Param(id = 10) @Nullable EmbeddingIndexingConfigParcel embeddingIndexingConfigParcel) {
         mName = Objects.requireNonNull(name);
         mDataType = dataType;
         mCardinality = cardinality;
@@ -96,6 +111,147 @@
         mDocumentIndexingConfigParcel = documentIndexingConfigParcel;
         mIntegerIndexingConfigParcel = integerIndexingConfigParcel;
         mJoinableConfigParcel = joinableConfigParcel;
+        mDescription = Objects.requireNonNull(description);
+        mEmbeddingIndexingConfigParcel = embeddingIndexingConfigParcel;
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for String. */
+    @NonNull
+    public static PropertyConfigParcel createForString(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality,
+            @NonNull StringIndexingConfigParcel stringIndexingConfigParcel,
+            @NonNull JoinableConfigParcel joinableConfigParcel) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_STRING,
+                cardinality,
+                /*schemaType=*/ null,
+                Objects.requireNonNull(stringIndexingConfigParcel),
+                /*documentIndexingConfigParcel=*/ null,
+                /*integerIndexingConfigParcel=*/ null,
+                Objects.requireNonNull(joinableConfigParcel),
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Long. */
+    @NonNull
+    public static PropertyConfigParcel createForLong(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality,
+            @AppSearchSchema.LongPropertyConfig.IndexingType int indexingType) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_LONG,
+                cardinality,
+                /*schemaType=*/ null,
+                /*stringIndexingConfigParcel=*/ null,
+                /*documentIndexingConfigParcel=*/ null,
+                new IntegerIndexingConfigParcel(indexingType),
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Double. */
+    @NonNull
+    public static PropertyConfigParcel createForDouble(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE,
+                cardinality,
+                /*schemaType=*/ null,
+                /*stringIndexingConfigParcel=*/ null,
+                /*documentIndexingConfigParcel=*/ null,
+                /*integerIndexingConfigParcel=*/ null,
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Boolean. */
+    @NonNull
+    public static PropertyConfigParcel createForBoolean(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN,
+                cardinality,
+                /*schemaType=*/ null,
+                /*stringIndexingConfigParcel=*/ null,
+                /*documentIndexingConfigParcel=*/ null,
+                /*integerIndexingConfigParcel=*/ null,
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Bytes. */
+    @NonNull
+    public static PropertyConfigParcel createForBytes(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES,
+                cardinality,
+                /*schemaType=*/ null,
+                /*stringIndexingConfigParcel=*/ null,
+                /*documentIndexingConfigParcel=*/ null,
+                /*integerIndexingConfigParcel=*/ null,
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Document. */
+    @NonNull
+    public static PropertyConfigParcel createForDocument(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality,
+            @NonNull String schemaType,
+            @NonNull DocumentIndexingConfigParcel documentIndexingConfigParcel) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT,
+                cardinality,
+                Objects.requireNonNull(schemaType),
+                /*stringIndexingConfigParcel=*/ null,
+                Objects.requireNonNull(documentIndexingConfigParcel),
+                /*integerIndexingConfigParcel=*/ null,
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                /*embeddingIndexingConfigParcel=*/ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Embedding. */
+    @NonNull
+    public static PropertyConfigParcel createForEmbedding(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality,
+            @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING,
+                cardinality,
+                /*schemaType=*/ null,
+                /*stringIndexingConfigParcel=*/ null,
+                /*documentIndexingConfigParcel=*/ null,
+                /*integerIndexingConfigParcel=*/ null,
+                /*joinableConfigParcel=*/ null,
+                Objects.requireNonNull(description),
+                new EmbeddingIndexingConfigParcel(indexingType));
     }
 
     /** Gets name for the property. */
@@ -104,6 +260,12 @@
         return mName;
     }
 
+    /** Gets description for the property. */
+    @NonNull
+    public String getDescription() {
+        return mDescription;
+    }
+
     /** Gets data type for the property. */
     @DataType
     public int getDataType() {
@@ -146,11 +308,84 @@
         return mJoinableConfigParcel;
     }
 
+    /** Gets the {@link EmbeddingIndexingConfigParcel}. */
+    @Nullable
+    public EmbeddingIndexingConfigParcel getEmbeddingIndexingConfigParcel() {
+        return mEmbeddingIndexingConfigParcel;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof PropertyConfigParcel)) {
+            return false;
+        }
+        PropertyConfigParcel otherProperty = (PropertyConfigParcel) other;
+        return ObjectsCompat.equals(mName, otherProperty.mName)
+                && Objects.equals(mDescription, otherProperty.mDescription)
+                && ObjectsCompat.equals(mDataType, otherProperty.mDataType)
+                && ObjectsCompat.equals(mCardinality, otherProperty.mCardinality)
+                && ObjectsCompat.equals(mSchemaType, otherProperty.mSchemaType)
+                && ObjectsCompat.equals(
+                mStringIndexingConfigParcel, otherProperty.mStringIndexingConfigParcel)
+                && ObjectsCompat.equals(
+                mDocumentIndexingConfigParcel, otherProperty.mDocumentIndexingConfigParcel)
+                && ObjectsCompat.equals(
+                mIntegerIndexingConfigParcel, otherProperty.mIntegerIndexingConfigParcel)
+                && ObjectsCompat.equals(
+                mJoinableConfigParcel, otherProperty.mJoinableConfigParcel)
+                && ObjectsCompat.equals(
+                mEmbeddingIndexingConfigParcel, otherProperty.mEmbeddingIndexingConfigParcel);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode =
+                ObjectsCompat.hash(
+                        mName,
+                        mDescription,
+                        mDataType,
+                        mCardinality,
+                        mSchemaType,
+                        mStringIndexingConfigParcel,
+                        mDocumentIndexingConfigParcel,
+                        mIntegerIndexingConfigParcel,
+                        mJoinableConfigParcel,
+                        mEmbeddingIndexingConfigParcel);
+        }
+        return mHashCode;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "{name: " + mName
+                + ", description: " + mDescription
+                + ", dataType: " + mDataType
+                + ", cardinality: " + mCardinality
+                + ", schemaType: " + mSchemaType
+                + ", stringIndexingConfigParcel: " + mStringIndexingConfigParcel
+                + ", documentIndexingConfigParcel: " + mDocumentIndexingConfigParcel
+                + ", integerIndexingConfigParcel: " + mIntegerIndexingConfigParcel
+                + ", joinableConfigParcel: " + mJoinableConfigParcel
+                + ", embeddingIndexingConfigParcel: " + mEmbeddingIndexingConfigParcel
+                + "}";
+    }
+
     /** Class to hold join configuration for a String type. */
     @SafeParcelable.Class(creator = "JoinableConfigParcelCreator")
     public static class JoinableConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final JoinableConfigParcelCreator CREATOR = new JoinableConfigParcelCreator();
+        public static final Parcelable.Creator<JoinableConfigParcel> CREATOR =
+                new JoinableConfigParcelCreator();
 
         @JoinableValueType
         @Field(id = 1, getter = "getJoinableValueType")
@@ -183,13 +418,38 @@
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             JoinableConfigParcelCreator.writeToParcel(this, dest, flags);
         }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hash(mJoinableValueType, mDeletionPropagation);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof JoinableConfigParcel)) {
+                return false;
+            }
+            JoinableConfigParcel otherObject = (JoinableConfigParcel) other;
+            return ObjectsCompat.equals(mJoinableValueType, otherObject.mJoinableValueType)
+                    && ObjectsCompat.equals(mDeletionPropagation, otherObject.mDeletionPropagation);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{joinableValueType: " + mJoinableValueType
+                    + ", deletePropagation " + mDeletionPropagation + "}";
+        }
     }
 
     /** Class to hold configuration a string type. */
     @SafeParcelable.Class(creator = "StringIndexingConfigParcelCreator")
     public static class StringIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final StringIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<StringIndexingConfigParcel> CREATOR =
                 new StringIndexingConfigParcelCreator();
 
         @AppSearchSchema.StringPropertyConfig.IndexingType
@@ -225,13 +485,38 @@
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             StringIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
         }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hash(mIndexingType, mTokenizerType);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof StringIndexingConfigParcel)) {
+                return false;
+            }
+            StringIndexingConfigParcel otherObject = (StringIndexingConfigParcel) other;
+            return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType)
+                    && ObjectsCompat.equals(mTokenizerType, otherObject.mTokenizerType);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{indexingType: " + mIndexingType
+                    + ", tokenizerType " + mTokenizerType + "}";
+        }
     }
 
     /** Class to hold configuration for integer property type. */
     @SafeParcelable.Class(creator = "IntegerIndexingConfigParcelCreator")
     public static class IntegerIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final IntegerIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<IntegerIndexingConfigParcel> CREATOR =
                 new IntegerIndexingConfigParcelCreator();
 
         @AppSearchSchema.LongPropertyConfig.IndexingType
@@ -255,13 +540,36 @@
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             IntegerIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
         }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hashCode(mIndexingType);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof IntegerIndexingConfigParcel)) {
+                return false;
+            }
+            IntegerIndexingConfigParcel otherObject = (IntegerIndexingConfigParcel) other;
+            return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{indexingType: " + mIndexingType + "}";
+        }
     }
 
     /** Class to hold configuration for document property type. */
     @SafeParcelable.Class(creator = "DocumentIndexingConfigParcelCreator")
     public static class DocumentIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final DocumentIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<DocumentIndexingConfigParcel> CREATOR =
                 new DocumentIndexingConfigParcelCreator();
 
         @Field(id = 1, getter = "shouldIndexNestedProperties")
@@ -295,10 +603,86 @@
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             DocumentIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
         }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hash(mIndexNestedProperties, mIndexableNestedPropertiesList);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof DocumentIndexingConfigParcel)) {
+                return false;
+            }
+            DocumentIndexingConfigParcel otherObject = (DocumentIndexingConfigParcel) other;
+            return ObjectsCompat.equals(mIndexNestedProperties, otherObject.mIndexNestedProperties)
+                    && ObjectsCompat.equals(mIndexableNestedPropertiesList,
+                    otherObject.mIndexableNestedPropertiesList);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{indexNestedProperties: " + mIndexNestedProperties
+                    + ", indexableNestedPropertiesList: " + mIndexableNestedPropertiesList
+                    + "}";
+        }
     }
 
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
+    /** Class to hold configuration for embedding property. */
+    @SafeParcelable.Class(creator = "EmbeddingIndexingConfigParcelCreator")
+    public static class EmbeddingIndexingConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final Parcelable.Creator<EmbeddingIndexingConfigParcel> CREATOR =
+                new EmbeddingIndexingConfigParcelCreator();
+
+        @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+        @Field(id = 1, getter = "getIndexingType")
+        private final int mIndexingType;
+
+        /** Constructor for {@link EmbeddingIndexingConfigParcel}. */
+        @Constructor
+        public EmbeddingIndexingConfigParcel(
+                @Param(id = 1) @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+                int indexingType) {
+            mIndexingType = indexingType;
+        }
+
+        /** Gets the indexing type for this embedding property. */
+        @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+        public int getIndexingType() {
+            return mIndexingType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            EmbeddingIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+
+        @Override
+        public int hashCode() {
+            return ObjectsCompat.hashCode(mIndexingType);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof EmbeddingIndexingConfigParcel)) {
+                return false;
+            }
+            EmbeddingIndexingConfigParcel otherObject = (EmbeddingIndexingConfigParcel) other;
+            return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{indexingType: " + mIndexingType + "}";
+        }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
index 3762adc..3e4fb3a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
@@ -17,12 +17,15 @@
 package androidx.appsearch.safeparcel;
 
 
+import android.annotation.SuppressLint;
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.EmbeddingVector;
 
 import java.util.Arrays;
 import java.util.Objects;
@@ -36,8 +39,11 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @SafeParcelable.Class(creator = "PropertyParcelCreator")
-public final class PropertyParcel extends AbstractSafeParcelable {
-    @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
+// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
+@SuppressLint("BanParcelableUsage")
+public final class PropertyParcel extends AbstractSafeParcelable implements Parcelable {
+    @NonNull public static final Parcelable.Creator<PropertyParcel> CREATOR =
+            new PropertyParcelCreator();
 
     @NonNull
     @Field(id = 1, getter = "getPropertyName")
@@ -67,6 +73,10 @@
     @Field(id = 7, getter = "getDocumentValues")
     private final GenericDocumentParcel[] mDocumentValues;
 
+    @Nullable
+    @Field(id = 8, getter = "getEmbeddingValues")
+    private final EmbeddingVector[] mEmbeddingValues;
+
     @Nullable private Integer mHashCode;
 
     @Constructor
@@ -77,7 +87,8 @@
             @Param(id = 4) @Nullable double[] doubleValues,
             @Param(id = 5) @Nullable boolean[] booleanValues,
             @Param(id = 6) @Nullable byte[][] bytesValues,
-            @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues) {
+            @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues,
+            @Param(id = 8) @Nullable EmbeddingVector[] embeddingValues) {
         mPropertyName = Objects.requireNonNull(propertyName);
         mStringValues = stringValues;
         mLongValues = longValues;
@@ -85,6 +96,7 @@
         mBooleanValues = booleanValues;
         mBytesValues = bytesValues;
         mDocumentValues = documentValues;
+        mEmbeddingValues = embeddingValues;
         checkOnlyOneArrayCanBeSet();
     }
 
@@ -130,6 +142,12 @@
         return mDocumentValues;
     }
 
+    /** Returns {@link EmbeddingVector}s in an array. */
+    @Nullable
+    public EmbeddingVector[] getEmbeddingValues() {
+        return mEmbeddingValues;
+    }
+
     /**
      * Returns the held values in an array for this property.
      *
@@ -155,6 +173,9 @@
         if (mDocumentValues != null) {
             return mDocumentValues;
         }
+        if (mEmbeddingValues != null) {
+            return mEmbeddingValues;
+        }
         return null;
     }
 
@@ -183,6 +204,9 @@
         if (mDocumentValues != null) {
             ++notNullCount;
         }
+        if (mEmbeddingValues != null) {
+            ++notNullCount;
+        }
         if (notNullCount == 0 || notNullCount > 1) {
             throw new IllegalArgumentException(
                     "One and only one type array can be set in PropertyParcel");
@@ -205,6 +229,8 @@
                 hashCode = Arrays.deepHashCode(mBytesValues);
             } else if (mDocumentValues != null) {
                 hashCode = Arrays.hashCode(mDocumentValues);
+            } else if (mEmbeddingValues != null) {
+                hashCode = Arrays.deepHashCode(mEmbeddingValues);
             }
             mHashCode = Objects.hash(mPropertyName, hashCode);
         }
@@ -228,7 +254,13 @@
                 && Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
                 && Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
                 && Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
-                && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues);
+                && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues)
+                && Arrays.deepEquals(mEmbeddingValues, otherPropertyParcel.mEmbeddingValues);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        PropertyParcelCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link PropertyParcel}. */
@@ -240,12 +272,14 @@
         private boolean[] mBooleanValues;
         private byte[][] mBytesValues;
         private GenericDocumentParcel[] mDocumentValues;
+        private EmbeddingVector[] mEmbeddingValues;
 
         public Builder(@NonNull String propertyName) {
             mPropertyName = Objects.requireNonNull(propertyName);
         }
 
         /** Sets String values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setStringValues(@NonNull String[] stringValues) {
             mStringValues = Objects.requireNonNull(stringValues);
@@ -253,6 +287,7 @@
         }
 
         /** Sets long values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setLongValues(@NonNull long[] longValues) {
             mLongValues = Objects.requireNonNull(longValues);
@@ -260,6 +295,7 @@
         }
 
         /** Sets double values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setDoubleValues(@NonNull double[] doubleValues) {
             mDoubleValues = Objects.requireNonNull(doubleValues);
@@ -267,6 +303,7 @@
         }
 
         /** Sets boolean values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
             mBooleanValues = Objects.requireNonNull(booleanValues);
@@ -274,6 +311,7 @@
         }
 
         /** Sets a two dimension byte array. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setBytesValues(@NonNull byte[][] bytesValues) {
             mBytesValues = Objects.requireNonNull(bytesValues);
@@ -281,12 +319,21 @@
         }
 
         /** Sets document values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setDocumentValues(@NonNull GenericDocumentParcel[] documentValues) {
             mDocumentValues = Objects.requireNonNull(documentValues);
             return this;
         }
 
+        /** Sets embedding values. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setEmbeddingValues(@NonNull EmbeddingVector[] embeddingValues) {
+            mEmbeddingValues = Objects.requireNonNull(embeddingValues);
+            return this;
+        }
+
         /** Builds a {@link PropertyParcel}. */
         @NonNull
         public PropertyParcel build() {
@@ -297,12 +344,8 @@
                     mDoubleValues,
                     mBooleanValues,
                     mBytesValues,
-                    mDocumentValues);
+                    mDocumentValues,
+                    mEmbeddingValues);
         }
     }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        PropertyParcelCreator.writeToParcel(this, dest, flags);
-    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
new file mode 100644
index 0000000..46be473
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.safeparcel;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An implemented creator for {@link PropertyParcel}.
+ *
+ * <p>In Jetpack, in order to serialize
+ * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument},
+ * {@link PropertyParcel} needs to be a real {@link Parcelable}.
+ */
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PropertyParcelCreator implements Parcelable.Creator<PropertyParcel> {
+    private static final String PROPERTY_NAME_FIELD = "propertyName";
+    private static final String STRING_ARRAY_FIELD = "stringArray";
+    private static final String LONG_ARRAY_FIELD = "longArray";
+    private static final String DOUBLE_ARRAY_FIELD = "doubleArray";
+    private static final String BOOLEAN_ARRAY_FIELD = "booleanArray";
+    // 1d
+    private static final String BYTE_ARRAY_FIELD = "byteArray";
+    // 2d
+    private static final String BYTES_ARRAY_FIELD = "bytesArray";
+    private static final String DOC_ARRAY_FIELD = "docArray";
+    private static final String EMBEDDING_VALUE_FIELD = "embeddingValue";
+    private static final String EMBEDDING_MODEL_SIGNATURE_FIELD = "embeddingModelSignature";
+    private static final String EMBEDDING_ARRAY_FIELD = "embeddingArray";
+
+    public PropertyParcelCreator() {
+    }
+
+    /** Creates a {@link PropertyParcel} from a {@link Bundle}. */
+    @SuppressWarnings({"unchecked"})
+    @NonNull
+    private static PropertyParcel createPropertyParcelFromBundle(
+            @NonNull Bundle propertyParcelBundle) {
+        Objects.requireNonNull(propertyParcelBundle);
+        String propertyName = propertyParcelBundle.getString(PROPERTY_NAME_FIELD);
+
+        Objects.requireNonNull(propertyName);
+        PropertyParcel.Builder builder = new PropertyParcel.Builder(propertyName);
+
+        // Get the values out of the bundle.
+        String[] stringValues = propertyParcelBundle.getStringArray(STRING_ARRAY_FIELD);
+        long[] longValues = propertyParcelBundle.getLongArray(LONG_ARRAY_FIELD);
+        double[] doubleValues = propertyParcelBundle.getDoubleArray(DOUBLE_ARRAY_FIELD);
+        boolean[] booleanValues = propertyParcelBundle.getBooleanArray(BOOLEAN_ARRAY_FIELD);
+
+        List<Bundle> bytesArray;
+        // SuppressWarnings can be applied on a local variable, but not any single line of
+        // code.
+        @SuppressWarnings("deprecation")
+        List<Bundle> tmpList = propertyParcelBundle.getParcelableArrayList(BYTES_ARRAY_FIELD);
+        bytesArray = tmpList;
+
+        Parcelable[] docValues;
+        // SuppressWarnings can be applied on a local variable, but not any single line of
+        // code.
+        @SuppressWarnings("deprecation")
+        Parcelable[] tmpParcel = propertyParcelBundle.getParcelableArray(DOC_ARRAY_FIELD);
+        docValues = tmpParcel;
+
+        // SuppressWarnings can be applied on a local variable, but not any single line of
+        // code.
+        @SuppressWarnings("deprecation")
+        List<Bundle> embeddingArray = propertyParcelBundle.getParcelableArrayList(
+                EMBEDDING_ARRAY_FIELD);
+
+        // Only one of those values will be set.
+        boolean valueSet = false;
+        if (stringValues != null) {
+            builder.setStringValues(stringValues);
+            valueSet = true;
+        } else if (longValues != null) {
+            builder.setLongValues(longValues);
+            valueSet = true;
+        } else if (doubleValues != null) {
+            builder.setDoubleValues(doubleValues);
+            valueSet = true;
+        } else if (booleanValues != null) {
+            builder.setBooleanValues(booleanValues);
+            valueSet = true;
+        } else if (bytesArray != null) {
+            byte[][] bytes = new byte[bytesArray.size()][];
+            for (int i = 0; i < bytesArray.size(); i++) {
+                Bundle byteArray = bytesArray.get(i);
+                if (byteArray == null) {
+                    continue;
+                }
+                byte[] innerBytes = byteArray.getByteArray(BYTE_ARRAY_FIELD);
+                if (innerBytes == null) {
+                    continue;
+                }
+                bytes[i] = innerBytes;
+            }
+            builder.setBytesValues(bytes);
+            valueSet = true;
+        } else if (docValues != null && docValues.length > 0) {
+            GenericDocumentParcel[] documentParcels =
+                    new GenericDocumentParcel[docValues.length];
+            System.arraycopy(docValues, 0, documentParcels, 0, docValues.length);
+            builder.setDocumentValues(documentParcels);
+            valueSet = true;
+        } else if (embeddingArray != null) {
+            EmbeddingVector[] embeddings = new EmbeddingVector[embeddingArray.size()];
+            for (int i = 0; i < embeddingArray.size(); i++) {
+                Bundle embeddingBundle = embeddingArray.get(i);
+                if (embeddingBundle == null) {
+                    continue;
+                }
+                float[] values = embeddingBundle.getFloatArray(EMBEDDING_VALUE_FIELD);
+                String modelSignature = embeddingBundle.getString(EMBEDDING_MODEL_SIGNATURE_FIELD);
+                if (values == null || modelSignature == null) {
+                    continue;
+                }
+                embeddings[i] = new EmbeddingVector(values, modelSignature);
+            }
+            builder.setEmbeddingValues(embeddings);
+            valueSet = true;
+        }
+
+        if (!valueSet) {
+            throw new IllegalArgumentException("property bundle passed in doesn't have any "
+                    + "value set.");
+        }
+
+        return builder.build();
+    }
+
+    /** Creates a {@link Bundle} from a {@link PropertyParcel}. */
+    @NonNull
+    private static Bundle createBundleFromPropertyParcel(
+            @NonNull PropertyParcel propertyParcel) {
+        Objects.requireNonNull(propertyParcel);
+        Bundle propertyParcelBundle = new Bundle();
+        propertyParcelBundle.putString(PROPERTY_NAME_FIELD, propertyParcel.getPropertyName());
+
+        // Check and set the properties
+        String[] stringValues = propertyParcel.getStringValues();
+        long[] longValues = propertyParcel.getLongValues();
+        double[] doubleValues = propertyParcel.getDoubleValues();
+        boolean[] booleanValues = propertyParcel.getBooleanValues();
+        byte[][] bytesArray = propertyParcel.getBytesValues();
+        GenericDocumentParcel[] docArray = propertyParcel.getDocumentValues();
+        EmbeddingVector[] embeddingArray = propertyParcel.getEmbeddingValues();
+
+        if (stringValues != null) {
+            propertyParcelBundle.putStringArray(STRING_ARRAY_FIELD, stringValues);
+        } else if (longValues != null) {
+            propertyParcelBundle.putLongArray(LONG_ARRAY_FIELD, longValues);
+        } else if (doubleValues != null) {
+            propertyParcelBundle.putDoubleArray(DOUBLE_ARRAY_FIELD, doubleValues);
+        } else if (booleanValues != null) {
+            propertyParcelBundle.putBooleanArray(BOOLEAN_ARRAY_FIELD, booleanValues);
+        } else if (bytesArray != null) {
+            ArrayList<Bundle> bundles = new ArrayList<>(bytesArray.length);
+            for (int i = 0; i < bytesArray.length; i++) {
+                Bundle byteArray = new Bundle();
+                byteArray.putByteArray(BYTE_ARRAY_FIELD, bytesArray[i]);
+                bundles.add(byteArray);
+            }
+            propertyParcelBundle.putParcelableArrayList(BYTES_ARRAY_FIELD, bundles);
+        } else if (docArray != null) {
+            propertyParcelBundle.putParcelableArray(DOC_ARRAY_FIELD, docArray);
+        } else if (embeddingArray != null) {
+            ArrayList<Bundle> bundles = new ArrayList<>(embeddingArray.length);
+            for (int i = 0; i < embeddingArray.length; i++) {
+                Bundle embedding = new Bundle();
+                embedding.putFloatArray(EMBEDDING_VALUE_FIELD, embeddingArray[i].getValues());
+                embedding.putString(EMBEDDING_MODEL_SIGNATURE_FIELD,
+                        embeddingArray[i].getModelSignature());
+                bundles.add(embedding);
+            }
+            propertyParcelBundle.putParcelableArrayList(EMBEDDING_ARRAY_FIELD, bundles);
+        }
+
+        return propertyParcelBundle;
+    }
+
+    @NonNull
+    @Override
+    public PropertyParcel createFromParcel(Parcel in) {
+        Bundle bundle = in.readBundle(getClass().getClassLoader());
+        return createPropertyParcelFromBundle(bundle);
+    }
+
+    @Override
+    public PropertyParcel[] newArray(int size) {
+        return new PropertyParcel[size];
+    }
+
+    /** Writes a {@link PropertyParcel} to a {@link Parcel}. */
+    public static void writeToParcel(@NonNull PropertyParcel propertyParcel,
+            @NonNull android.os.Parcel parcel, int flags) {
+        parcel.writeBundle(createBundleFromPropertyParcel(propertyParcel));
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
index fa70911..8a25a36 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
@@ -74,4 +74,7 @@
          */
         boolean doNotParcelTypeDefaultValues() default false;
     }
+
+    /** Provide same interface as {@link android.os.Parcelable} for code sync purpose. */
+    int describeContents();
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
index 22755d0..53afaa7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
@@ -17,6 +17,7 @@
 package androidx.appsearch.safeparcel.stub;
 
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -31,7 +32,21 @@
  */
 // @exportToFramework:skipFile()
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class AbstractCreator {
+abstract class AbstractCreator<T> implements Parcelable.Creator<T> {
+    @Override
+    public T createFromParcel(Parcel var1) {
+        // This is here only for code sync purpose.
+        throw new UnsupportedOperationException("createFromParcel is not implemented and should "
+                + "not be used.");
+    }
+
+    @Override
+    public T[] newArray(int var1) {
+        // This is here only for code sync purpose.
+        throw new UnsupportedOperationException("newArray is not implemented and should "
+                + "not be used.");
+    }
+
     public static void writeToParcel(
             @NonNull SafeParcelable safeParcelable,
             @NonNull Parcel parcel,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
index 97887bf..48d197c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
@@ -16,9 +16,34 @@
 package androidx.appsearch.safeparcel.stub;
 
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.JoinSpec;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResult.MatchInfo;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SearchSuggestionResult;
+import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.SetSchemaResponse.MigrationFailure;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.safeparcel.PropertyConfigParcel;
-import androidx.appsearch.safeparcel.PropertyParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.EmbeddingIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.IntegerIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
+import androidx.appsearch.stats.SchemaMigrationStats;
 
 /**
  * Stub creators for any classes extending
@@ -29,61 +54,145 @@
  * be provided for code sync purpose.
  */
 // @exportToFramework:skipFile()
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class StubCreators {
     /** Stub creator for {@link androidx.appsearch.app.StorageInfo}. */
-    public static class StorageInfoCreator extends AbstractCreator {
-    }
-
-    /** Stub creator for {@link PropertyParcel}. */
-    public static class PropertyParcelCreator extends AbstractCreator {
+    public static class StorageInfoCreator extends AbstractCreator<StorageInfo> {
     }
 
     /** Stub creator for {@link PropertyConfigParcel}. */
-    public static class PropertyConfigParcelCreator extends AbstractCreator {
+    public static class PropertyConfigParcelCreator extends AbstractCreator<PropertyConfigParcel> {
     }
 
     /**
      * Stub creator for
      * {@link PropertyConfigParcel.JoinableConfigParcel}.
      */
-    public static class JoinableConfigParcelCreator extends AbstractCreator {
+    public static class JoinableConfigParcelCreator extends AbstractCreator<JoinableConfigParcel> {
     }
 
     /**
      * Stub creator for
      * {@link PropertyConfigParcel.StringIndexingConfigParcel}.
      */
-    public static class StringIndexingConfigParcelCreator extends AbstractCreator {
+    public static class StringIndexingConfigParcelCreator extends
+            AbstractCreator<StringIndexingConfigParcel> {
     }
 
     /**
      * Stub creator for
      * {@link PropertyConfigParcel.IntegerIndexingConfigParcel}.
      */
-    public static class IntegerIndexingConfigParcelCreator extends AbstractCreator {
+    public static class IntegerIndexingConfigParcelCreator extends
+            AbstractCreator<IntegerIndexingConfigParcel> {
     }
 
     /**
      * Stub creator for
      * {@link PropertyConfigParcel.DocumentIndexingConfigParcel}.
      */
-    public static class DocumentIndexingConfigParcelCreator extends AbstractCreator {
+    public static class DocumentIndexingConfigParcelCreator extends
+            AbstractCreator<DocumentIndexingConfigParcel> {
     }
 
-    /** Stub creator for {@link GenericDocumentParcel}. */
-    public static class GenericDocumentParcelCreator extends AbstractCreator {
+    /** Stub creator for {@link SchemaVisibilityConfig}. */
+    public static class VisibilityConfigCreator extends AbstractCreator<SchemaVisibilityConfig> {
     }
 
-    /** Stub creator for {@link androidx.appsearch.app.VisibilityPermissionDocument}. */
-    public static class VisibilityPermissionDocumentCreator extends AbstractCreator {
+    /**
+     * Stub creator for {@link EmbeddingIndexingConfigParcel}.
+     */
+    public static class EmbeddingIndexingConfigParcelCreator extends
+            AbstractCreator<EmbeddingIndexingConfigParcel> {
     }
 
-    /** Stub creator for {@link androidx.appsearch.app.VisibilityDocument}. */
-    public static class VisibilityDocumentCreator extends AbstractCreator {
+    /** Stub creator for {@link InternalVisibilityConfig}. */
+    public static class InternalVisibilityConfigCreator
+            extends AbstractCreator<InternalVisibilityConfig> {
+    }
+
+    /** Stub creator for {@link VisibilityPermissionConfig}. */
+    public static class VisibilityPermissionConfigCreator extends
+            AbstractCreator<VisibilityPermissionConfig> {
     }
 
     /** Stub creator for {@link androidx.appsearch.stats.SchemaMigrationStats}. */
-    public static class SchemaMigrationStatsCreator extends AbstractCreator {
+    public static class SchemaMigrationStatsCreator extends AbstractCreator<SchemaMigrationStats> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionResult}. */
+    public static class SearchSuggestionResultCreator extends
+            AbstractCreator<SearchSuggestionResult> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionSpec}. */
+    public static class SearchSuggestionSpecCreator extends AbstractCreator<SearchSuggestionSpec> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.observer.ObserverSpec}. */
+    public static class ObserverSpecCreator extends AbstractCreator<ObserverSpec> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse}. */
+    public static class SetSchemaResponseCreator extends
+            AbstractCreator<SetSchemaResponse> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}. */
+    public static class MigrationFailureCreator extends
+            AbstractCreator<MigrationFailure> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.InternalSetSchemaResponse}. */
+    public static class InternalSetSchemaResponseCreator extends
+            AbstractCreator<InternalSetSchemaResponse> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SearchSpec}. */
+    public static class SearchSpecCreator extends AbstractCreator<SearchSpec> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.JoinSpec}. */
+    public static class JoinSpecCreator extends AbstractCreator<JoinSpec> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.GetSchemaResponse}. */
+    public static class GetSchemaResponseCreator extends AbstractCreator<GetSchemaResponse> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.AppSearchSchema}. */
+    public static class AppSearchSchemaCreator extends
+            AbstractCreator<AppSearchSchema> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SearchResult}. */
+    public static class SearchResultCreator extends AbstractCreator<SearchResult> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.MatchInfo}. */
+    public static class MatchInfoCreator extends AbstractCreator<MatchInfo> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.SearchResultPage}. */
+    public static class SearchResultPageCreator extends AbstractCreator<SearchResultPage> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.RemoveByDocumentIdRequest}. */
+    public static class RemoveByDocumentIdRequestCreator extends
+            AbstractCreator<RemoveByDocumentIdRequest> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.ReportUsageRequest}. */
+    public static class ReportUsageRequestCreator extends AbstractCreator<ReportUsageRequest> {
+    }
+
+    /** Stub creator for {@link androidx.appsearch.app.GetByDocumentIdRequest}. */
+    public static class GetByDocumentIdRequestCreator extends
+            AbstractCreator<GetByDocumentIdRequest> {
+    }
+
+    /** Stub creator for {@link EmbeddingVector}. */
+    public static class EmbeddingVectorCreator extends
+            AbstractCreator<EmbeddingVector> {
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
index 0a24293..91b74e7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
@@ -17,6 +17,7 @@
 package androidx.appsearch.stats;
 
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -40,10 +41,10 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @SafeParcelable.Class(creator = "SchemaMigrationStatsCreator")
 public final class SchemaMigrationStats extends AbstractSafeParcelable {
-    @NonNull public static final SchemaMigrationStatsCreator CREATOR =
+    @NonNull public static final Parcelable.Creator<SchemaMigrationStats> CREATOR =
             new SchemaMigrationStatsCreator();
 
-    // Indicate the how a SetSchema call relative to SchemaMigration case.
+    /** Indicate the SetSchema call type relative to SchemaMigration case. */
     @IntDef(
             value = {
                     NO_MIGRATION,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
new file mode 100644
index 0000000..0fadd2c
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.usagereporting;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Wrapper class for action constants.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ActionConstants {
+    /**
+     * Unknown action type.
+     *
+     * <p>It is defined for abstract action class and compatibility, so it should not be used in any
+     * concrete instances.
+     */
+    public static final int ACTION_TYPE_UNKNOWN = 0;
+
+    /**
+     * Search action type.
+     *
+     * <!--@exportToFramework:ifJetpack()-->
+     * <p>It is the action type for {@link SearchAction}.
+     * <!--@exportToFramework:else()-->
+     */
+    public static final int ACTION_TYPE_SEARCH = 1;
+
+    /**
+     * Click action type.
+     *
+     * <!--@exportToFramework:ifJetpack()-->
+     * <p>It is the action type for {@link ClickAction}.
+     * <!--@exportToFramework:else()-->
+     */
+    public static final int ACTION_TYPE_CLICK = 2;
+
+    private ActionConstants() {}
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
new file mode 100644
index 0000000..e69320a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.Features;
+import androidx.core.util.Preconditions;
+
+/**
+ * {@link ClickAction} is a built-in AppSearch document type that contains different metrics.
+ * Clients can report the user's click actions on a {@link androidx.appsearch.app.SearchResult}
+ * document.
+ *
+ * <p>In order to use this document type, the client must explicitly set this schema type via
+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}.
+ *
+ * <p>Click actions can be used as signals to boost ranking via
+ * {@link androidx.appsearch.app.JoinSpec} API in future search requests.
+ *
+ * <p>Since {@link ClickAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The
+ * default TTL is 60 days.
+ */
+// In ClickAction document, there is a joinable property "referencedQualifiedId" for reporting the
+// qualified id of the clicked document. The client can create personal navboost with click action
+// signals by join query with this property. Therefore, ClickAction document class requires join
+// feature.
+@RequiresFeature(
+        enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+        name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
+@Document(name = "builtin:ClickAction")
+public class ClickAction extends TakenAction {
+    @Nullable
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    private final String mQuery;
+
+    @Nullable
+    @Document.StringProperty(joinableValueType =
+            StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+    private final String mReferencedQualifiedId;
+
+    @Document.LongProperty
+    private final int mResultRankInBlock;
+
+    @Document.LongProperty
+    private final int mResultRankGlobal;
+
+    @Document.LongProperty
+    private final long mTimeStayOnResultMillis;
+
+    ClickAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+            long actionTimestampMillis, @TakenAction.ActionType int actionType,
+            @Nullable String query, @Nullable String referencedQualifiedId, int resultRankInBlock,
+            int resultRankGlobal, long timeStayOnResultMillis) {
+        super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);
+
+        mQuery = query;
+        mReferencedQualifiedId = referencedQualifiedId;
+        mResultRankInBlock = resultRankInBlock;
+        mResultRankGlobal = resultRankGlobal;
+        mTimeStayOnResultMillis = timeStayOnResultMillis;
+    }
+
+    /**
+     * Returns the user-entered search input (without any operators or rewriting) that yielded the
+     * {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+     */
+    @Nullable
+    public String getQuery() {
+        return mQuery;
+    }
+
+    /**
+     * Returns the qualified id of the {@link androidx.appsearch.app.SearchResult} document that the
+     * user clicked on.
+     *
+     * <p>A qualified id is a string generated by package, database, namespace, and document id. See
+     * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(String,String,String,String)}
+     * for more details.
+     */
+    @Nullable
+    public String getReferencedQualifiedId() {
+        return mReferencedQualifiedId;
+    }
+
+    /**
+     * Returns the rank of the {@link androidx.appsearch.app.SearchResult} document among the
+     * user-defined block.
+     *
+     * <p>The client can define its own custom definition for block, e.g. corpus name, group, etc.
+     *
+     * <p>For example, a client defines the block as corpus, and AppSearch returns 5 documents with
+     * corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"]. Then the block ranks of
+     * them = [1, 2, 1, 1, 2].
+     *
+     * <p>If the client is not presenting the results in multiple blocks, they should set this value
+     * to match {@link #getResultRankGlobal}.
+     *
+     * <p>If unset, then the block rank of the {@link androidx.appsearch.app.SearchResult} document
+     * will be set to -1 to mark invalid.
+     */
+    public int getResultRankInBlock() {
+        return mResultRankInBlock;
+    }
+
+    /**
+     * Returns the global rank of the {@link androidx.appsearch.app.SearchResult} document.
+     *
+     * <p>Global rank reflects the order of {@link androidx.appsearch.app.SearchResult} documents
+     * returned by AppSearch.
+     *
+     * <p>For example, AppSearch returns 2 pages with 10 {@link androidx.appsearch.app.SearchResult}
+     * documents for each page. Then the global ranks of them will be 1 to 10 for the first page,
+     * and 11 to 20 for the second page.
+     *
+     * <p>If unset, then the global rank of the {@link androidx.appsearch.app.SearchResult} document
+     * will be set to -1 to mark invalid.
+     */
+    public int getResultRankGlobal() {
+        return mResultRankGlobal;
+    }
+
+    /**
+     * Returns the time in milliseconds that user stays on the
+     * {@link androidx.appsearch.app.SearchResult} document after clicking it.
+     */
+    public long getTimeStayOnResultMillis() {
+        return mTimeStayOnResultMillis;
+    }
+
+    // TODO(b/314026345): redesign builder to enable inheritance for ClickAction.
+    /** Builder for {@link ClickAction}. */
+    @Document.BuilderProducer
+    public static final class Builder extends BuilderImpl<Builder> {
+        private String mQuery;
+        private String mReferencedQualifiedId;
+        private int mResultRankInBlock;
+        private int mResultRankGlobal;
+        private long mTimeStayOnResultMillis;
+
+        /**
+         * Constructor for {@link ClickAction.Builder}.
+         *
+         * @param namespace             Namespace for the Document. See {@link Document.Namespace}.
+         * @param id                    Unique identifier for the Document. See {@link Document.Id}.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {
+            this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_CLICK);
+        }
+
+        /**
+         * Constructs {@link ClickAction.Builder} by copying existing values from the given
+         * {@link ClickAction}.
+         *
+         * @param clickAction an existing {@link ClickAction} object.
+         */
+        public Builder(@NonNull ClickAction clickAction) {
+            super(Preconditions.checkNotNull(clickAction));
+
+            mQuery = clickAction.getQuery();
+            mReferencedQualifiedId = clickAction.getReferencedQualifiedId();
+            mResultRankInBlock = clickAction.getResultRankInBlock();
+            mResultRankGlobal = clickAction.getResultRankGlobal();
+            mTimeStayOnResultMillis = clickAction.getTimeStayOnResultMillis();
+        }
+
+        /**
+         * Constructor for {@link ClickAction.Builder}.
+         *
+         * <p>It is required by {@link Document.BuilderProducer}.
+         *
+         * @param namespace             Namespace for the Document. See {@link Document.Namespace}.
+         * @param id                    Unique identifier for the Document. See {@link Document.Id}.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         * @param actionType            Action type enum for the Document. See
+         *                              {@link TakenAction.ActionType}.
+         */
+        Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+                @TakenAction.ActionType int actionType) {
+            super(namespace, id, actionTimestampMillis, actionType);
+
+            // Default for unset result rank fields. Since negative number is invalid for ranking,
+            // -1 is used as an unset value and AppSearch will ignore it.
+            mResultRankInBlock = -1;
+            mResultRankGlobal = -1;
+
+            // Default for unset timeStayOnResultMillis. Since negative number is invalid for
+            // time in millis, -1 is used as an unset value and AppSearch will ignore it.
+            mTimeStayOnResultMillis = -1;
+        }
+
+        /**
+         * Sets the user-entered search input (without any operators or rewriting) that yielded
+         * the {@link androidx.appsearch.app.SearchResult} on which the user clicked.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@Nullable String query) {
+            mQuery = query;
+            return this;
+        }
+
+        /**
+         * Sets the qualified id of the {@link androidx.appsearch.app.SearchResult} document that
+         * the user takes action on.
+         *
+         * <p>A qualified id is a string generated by package, database, namespace, and document id.
+         * See {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(
+         * String,String,String,String)} for more details.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setReferencedQualifiedId(@Nullable String referencedQualifiedId) {
+            mReferencedQualifiedId = referencedQualifiedId;
+            return this;
+        }
+
+        /**
+         * Sets the rank of the {@link androidx.appsearch.app.SearchResult} document among the
+         * user-defined block.
+         *
+         * @see ClickAction#getResultRankInBlock
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankInBlock(int resultRankInBlock) {
+            mResultRankInBlock = resultRankInBlock;
+            return this;
+        }
+
+        /**
+         * Sets the global rank of the {@link androidx.appsearch.app.SearchResult} document.
+         *
+         * @see ClickAction#getResultRankGlobal
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankGlobal(int resultRankGlobal) {
+            mResultRankGlobal = resultRankGlobal;
+            return this;
+        }
+
+        /**
+         * Sets the time in milliseconds that user stays on the
+         * {@link androidx.appsearch.app.SearchResult} document after clicking it.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+            mTimeStayOnResultMillis = timeStayOnResultMillis;
+            return this;
+        }
+
+        /** Builds a {@link ClickAction}. */
+        @Override
+        @NonNull
+        public ClickAction build() {
+            return new ClickAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,
+                    mActionType, mQuery, mReferencedQualifiedId, mResultRankInBlock,
+                    mResultRankGlobal, mTimeStayOnResultMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
new file mode 100644
index 0000000..67cb4ea
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * {@link SearchAction} is a built-in AppSearch document type that contains different metrics.
+ * <ul>
+ *     <li>Clients can report the user's search actions.
+ *     <li>Usually {@link SearchAction} is reported together with {@link ClickAction}, since the
+ *     user clicks on {@link androidx.appsearch.app.SearchResult} documents after searching.
+ * </ul>
+ *
+ * <p>In order to use this document type, the client must explicitly set this schema type via
+ * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}.
+ *
+ * <p>Since {@link SearchAction} is an AppSearch document, the client can handle deletion via
+ * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The
+ * default TTL is 60 days.
+ */
+@Document(name = "builtin:SearchAction")
+public class SearchAction extends TakenAction {
+    @Nullable
+    @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    private final String mQuery;
+
+    @Document.LongProperty
+    private final int mFetchedResultCount;
+
+    SearchAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+            long actionTimestampMillis, @TakenAction.ActionType int actionType,
+            @Nullable String query, int fetchedResultCount) {
+        super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType);
+
+        mQuery = query;
+        mFetchedResultCount = fetchedResultCount;
+    }
+
+    /** Returns the user-entered search input (without any operators or rewriting). */
+    @Nullable
+    public String getQuery() {
+        return mQuery;
+    }
+
+    /**
+     * Returns total number of results fetched from AppSearch by the client in this
+     * {@link SearchAction}.
+     *
+     * <p>If unset, then it will be set to -1 to mark invalid.
+     */
+    public int getFetchedResultCount() {
+        return mFetchedResultCount;
+    }
+
+    // TODO(b/314026345): redesign builder to enable inheritance for SearchAction.
+    /** Builder for {@link SearchAction}. */
+    @Document.BuilderProducer
+    public static final class Builder extends BuilderImpl<Builder> {
+        private String mQuery;
+        private int mFetchedResultCount;
+
+        /**
+         * Constructor for {@link SearchAction.Builder}.
+         *
+         * @param namespace             Namespace for the Document. See {@link Document.Namespace}.
+         * @param id                    Unique identifier for the Document. See {@link Document.Id}.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) {
+            this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_SEARCH);
+        }
+
+        /**
+         * Constructor for {@link Builder} with all the existing values.
+         */
+        public Builder(@NonNull SearchAction searchAction) {
+            super(Preconditions.checkNotNull(searchAction));
+
+            mQuery = searchAction.getQuery();
+            mFetchedResultCount = searchAction.getFetchedResultCount();
+        }
+
+        /**
+         * Constructor for {@link SearchAction.Builder}.
+         *
+         * <p>It is required by {@link Document.BuilderProducer}.
+         *
+         * @param namespace             Namespace for the Document. See {@link Document.Namespace}.
+         * @param id                    Unique identifier for the Document. See {@link Document.Id}.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         * @param actionType            Action type enum for the Document. See
+         *                              {@link TakenAction.ActionType}.
+         */
+        Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+                @TakenAction.ActionType int actionType) {
+            super(namespace, id, actionTimestampMillis, actionType);
+
+            // Default for unset fetchedResultCount. Since negative number is invalid for fetched
+            // result count, -1 is used as an unset value and AppSearch will ignore it.
+            mFetchedResultCount = -1;
+        }
+
+        /** Sets the user-entered search input (without any operators or rewriting). */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@Nullable String query) {
+            mQuery = query;
+            return this;
+        }
+
+        /**
+         * Sets total number of results fetched from AppSearch by the client in this
+         * {@link SearchAction}.
+         *
+         * @see SearchAction#getFetchedResultCount
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setFetchedResultCount(int fetchedResultCount) {
+            mFetchedResultCount = fetchedResultCount;
+            return this;
+        }
+
+        /** Builds a {@link SearchAction}. */
+        @Override
+        @NonNull
+        public SearchAction build() {
+            return new SearchAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis,
+                    mActionType, mQuery, mFetchedResultCount);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
new file mode 100644
index 0000000..da35743
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.usagereporting;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.annotation.Document;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * {@link TakenAction} is an abstract class which holds common fields of other AppSearch built-in
+ * action types (e.g. {@link SearchAction}, {@link ClickAction}).
+ *
+ * <p>Clients can report the user's actions by creating concrete actions with
+ * {@link androidx.appsearch.app.PutDocumentsRequest.Builder#addTakenActions} API.
+ */
+@Document(name = "builtin:TakenAction")
+public abstract class TakenAction {
+    /** Default TTL for all related {@link TakenAction} documents: 60 days. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final long DEFAULT_DOCUMENT_TTL_MILLIS = 60L * 24 * 60 * 60 * 1000;
+
+    /** AppSearch taken action type. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef(value = {
+            ActionConstants.ACTION_TYPE_UNKNOWN,
+            ActionConstants.ACTION_TYPE_SEARCH,
+            ActionConstants.ACTION_TYPE_CLICK,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ActionType {
+    }
+
+    @NonNull
+    @Document.Namespace
+    private final String mNamespace;
+
+    @NonNull
+    @Document.Id
+    private final String mId;
+
+    @Document.TtlMillis
+    private final long mDocumentTtlMillis;
+
+    @Document.CreationTimestampMillis
+    private final long mActionTimestampMillis;
+
+    @Document.LongProperty
+    @ActionType
+    private final int mActionType;
+
+    TakenAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis,
+            long actionTimestampMillis, @ActionType int actionType) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mId = Preconditions.checkNotNull(id);
+        mDocumentTtlMillis = documentTtlMillis;
+        mActionTimestampMillis = actionTimestampMillis;
+        mActionType = actionType;
+    }
+
+    /** Returns the namespace of the {@link TakenAction}. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the unique identifier of the {@link TakenAction}. */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the time-to-live (TTL) of the {@link TakenAction} document as a duration in
+     * milliseconds.
+     *
+     * <p>The document will be automatically deleted when the TTL expires (since
+     * {@link #getActionTimestampMillis()}).
+     *
+     * <p>The default TTL for {@link TakenAction} document is 60 days.
+     *
+     * <p>See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on TTL.
+     */
+    public long getDocumentTtlMillis() {
+        return mDocumentTtlMillis;
+    }
+
+    /**
+     * Returns the timestamp when the user took the action, in milliseconds since Unix epoch.
+     *
+     * <p>The action timestamp will be used together with {@link #getDocumentTtlMillis()} as the
+     * document retention.
+     */
+    public long getActionTimestampMillis() {
+        return mActionTimestampMillis;
+    }
+
+    /**
+     * Returns the action type of the {@link TakenAction}.
+     *
+     * @see TakenAction.ActionType
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @ActionType
+    public int getActionType() {
+        return mActionType;
+    }
+
+    // TODO(b/330777270): improve AnnotationProcessor for abstract document class, and remove this
+    //                    builder.
+    /** Builder for {@link TakenAction}. */
+    @Document.BuilderProducer
+    static final class Builder extends BuilderImpl<Builder> {
+        /**
+         * Constructor for {@link TakenAction.Builder}.
+         *
+         * @param namespace             Namespace for the Document. See {@link Document.Namespace}.
+         * @param id                    Unique identifier for the Document. See {@link Document.Id}.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         * @param actionType            Action type enum for the Document. See
+         *                              {@link TakenAction.ActionType}.
+         */
+        Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+                @TakenAction.ActionType int actionType) {
+            super(namespace, id, actionTimestampMillis, actionType);
+        }
+
+        /** Constructor for {@link TakenAction.Builder} with all the existing values. */
+        Builder(@NonNull TakenAction takenAction) {
+            super(takenAction);
+        }
+    }
+
+    // Use templated BuilderImpl to resolve base class setter return type issue for child class
+    // builder instances.
+    @SuppressWarnings("unchecked")
+    static class BuilderImpl<T extends BuilderImpl<T>> {
+        protected final String mNamespace;
+        protected final String mId;
+        protected long mDocumentTtlMillis;
+        protected long mActionTimestampMillis;
+        @ActionType
+        protected int mActionType;
+
+        /**
+         * Constructs {@link TakenAction.BuilderImpl} with given {@code namespace}, {@code id},
+         * {@code actionTimestampMillis} and {@code actionType}.
+         *
+         * @param namespace             The namespace of the {@link TakenAction} document.
+         * @param id                    The id of the {@link TakenAction} document.
+         * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds
+         *                              since Unix epoch.
+         * @param actionType            The action type enum of the Document.
+         */
+        BuilderImpl(@NonNull String namespace, @NonNull String id, long actionTimestampMillis,
+                @TakenAction.ActionType int actionType) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mId = Preconditions.checkNotNull(id);
+            mActionTimestampMillis = actionTimestampMillis;
+            mActionType = actionType;
+
+            // Default for documentTtlMillis.
+            mDocumentTtlMillis = TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS;
+        }
+
+        /**
+         * Constructs {@link TakenAction.BuilderImpl} by copying existing values from the given
+         * {@link TakenAction}.
+         *
+         * @param takenAction an existing {@link TakenAction} object.
+         */
+        BuilderImpl(@NonNull TakenAction takenAction) {
+            this(takenAction.getNamespace(), takenAction.getId(),
+                    takenAction.getActionTimestampMillis(), takenAction.getActionType());
+            mDocumentTtlMillis = takenAction.getDocumentTtlMillis();
+        }
+
+        /**
+         * Sets the time-to-live (TTL) of the {@link TakenAction} document as a duration in
+         * milliseconds.
+         *
+         * <p>The document will be automatically deleted when the TTL expires (since
+         * {@link TakenAction#getActionTimestampMillis()}).
+         *
+         * <p>The default TTL for {@link TakenAction} document is 60 days.
+         *
+         * <p>See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on
+         * TTL.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public T setDocumentTtlMillis(long documentTtlMillis) {
+            mDocumentTtlMillis = documentTtlMillis;
+            return (T) this;
+        }
+
+        // TODO(b/330777270): improve AnnotationProcessor for abstract document class builder, and
+        //                    make it an abstract method.
+        /**
+         * For AppSearch annotation processor requirement only. The client should never call it
+         * since it is impossible to instantiate an abstract class.
+         *
+         * @throws UnsupportedOperationException
+         */
+        @NonNull
+        public TakenAction build() {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
index 9d50086..7604db5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -248,7 +248,7 @@
             // Read bundle from bytes
             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
             parcel.setDataPosition(0);
-            return parcel.readBundle();
+            return parcel.readBundle(BundleUtil.class.getClassLoader());
         } finally {
             parcel.recycle();
         }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
new file mode 100644
index 0000000..c09ac7a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.util;
+
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utilities for handling exceptions.
+ */
+// This file has different behavior in Framework as compared to JetPack (like it is okay to rethrow
+// exception and log instead of rethrowing from SystemServer in case of RemoteException). This file
+// is not synced to Framework, as it maintains its own environment specific copy.
+// @exportToFramework:skipFile()
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ExceptionUtil {
+    private static final String TAG = "AppSearchExceptionUtil";
+
+    /**
+     * {@link RuntimeException} will be rethrown if {@link #isItOkayToRethrowException()} returns
+     * true.
+     */
+    public static void handleException(@NonNull Exception e) {
+        if (isItOkayToRethrowException() && e instanceof RuntimeException) {
+            rethrowRuntimeException((RuntimeException) e);
+        }
+    }
+
+    /** Returns whether it is OK to rethrow exceptions from this entrypoint. */
+    private static boolean isItOkayToRethrowException() {
+        return true;
+    }
+
+    /** Rethrow exception from SystemServer in Framework code. */
+    public static void handleRemoteException(@NonNull RemoteException e) {
+        Log.w(TAG, "Unable to make a call to AppSearchManagerService!", e);
+    }
+
+    /**
+     * A helper method to rethrow {@link RuntimeException}.
+     *
+     * <p>We use this to enforce exception type and assure the compiler/linter that the exception is
+     * indeed {@link RuntimeException} and can be rethrown safely.
+     */
+    private static void rethrowRuntimeException(RuntimeException e) {
+        throw e;
+    }
+
+    private ExceptionUtil() {}
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
index cc52cdd..0f3e100 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
@@ -22,6 +22,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.Size;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 
 /**
  * Utilities for logging to logcat.
@@ -33,6 +34,8 @@
     // TODO(b/232285376): If it becomes possible to detect an eng build, turn this on by default
     //  for eng builds.
     public static final boolean DEBUG = false;
+    public static final boolean INFO = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .isInfoLoggingEnabled();
 
     /**
      * The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in
@@ -92,7 +95,7 @@
             @NonNull String message,
             @Nullable Object fastTraceObj,
             @Nullable Object fullTraceObj) {
-        if (PII_TRACE_LEVEL == 0) {
+        if (PII_TRACE_LEVEL == 0 || !INFO) {
             return;
         }
         StringBuilder builder = new StringBuilder("(trace) ").append(message);
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
index dd8ae1d..39dd6c7 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
@@ -527,6 +527,13 @@
                         env,
                         /* allowRepeated= */true);
                 break;
+            case EMBEDDING_PROPERTY:
+                requireTypeIsOneOf(
+                        getterOrField,
+                        List.of(helper.mEmbeddingType),
+                        env,
+                        /* allowRepeated= */true);
+                break;
             default:
                 throw new IllegalStateException("Unhandled annotation: " + annotation);
         }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 9f11508..7296edd 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -203,8 +203,9 @@
         //       unboxing.
         //
         //   1b: ListCallArraysAsList
-        //       List contains String. We have to convert this from an array of String[], but no
-        //       conversion of the collection elements is needed. We can use Arrays#asList for this.
+        //       List contains String or EmbeddingVector. We have to convert this from
+        //       an array of String[] or EmbeddingVector[], but no conversion of the
+        //       collection elements is needed. We can use Arrays#asList for this.
         //
         //   1c: ListForLoopCallFromGenericDocument
         //       List contains a class which is annotated with @Document.
@@ -225,7 +226,8 @@
         //       of unboxing.
         //
         //   2b: ArrayUseDirectly
-        //       Array is of type String[], long[], double[], boolean[], byte[][].
+        //       Array is of type String[], long[], double[], boolean[], byte[][] or
+        //       EmbeddingVector[].
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallFromGenericDocument
@@ -243,7 +245,8 @@
 
         // Scenario 3: Single valued fields
         //   3a: FieldUseDirectlyWithNullCheck
-        //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
+        //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
+        //       EmbeddingVector.
         //       We can use this field directly, after testing for null. The java compiler will box
         //       or unbox as needed.
         //
@@ -393,6 +396,19 @@
                     default:
                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
                 }
+            case EMBEDDING_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION: // List<EmbeddingVector>: 1b
+                        return listCallArraysAsList(annotation, getterOrField);
+                    case ARRAY:
+                        // EmbeddingVector[]: 2b
+                        return arrayUseDirectly(annotation, getterOrField);
+                    case SINGLE:
+                        // EmbeddingVector: 3a
+                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
             default:
                 throw new IllegalStateException("Unhandled annotation: " + annotation);
         }
@@ -433,8 +449,9 @@
     }
 
     // 1b: ListCallArraysAsList
-    //     List contains String. We have to convert this from an array of String[], but no
-    //     conversion of the collection elements is needed. We can use Arrays#asList for this.
+    //     List contains String or EmbeddingVector. We have to convert this from
+    //     an array of String[] or EmbeddingVector[], but no conversion of the
+    //     collection elements is needed. We can use Arrays#asList for this.
     @NonNull
     private CodeBlock listCallArraysAsList(
             @NonNull DataPropertyAnnotation annotation,
@@ -559,8 +576,8 @@
     }
 
     // 2b: ArrayUseDirectly
-    //     Array is of type String[], long[], double[], boolean[], byte[][].
-    //     We can directly use this field with no conversion.
+    //     Array is of type String[], long[], double[], boolean[], byte[][] or
+    //     EmbeddingVector[].
     @NonNull
     private CodeBlock arrayUseDirectly(
             @NonNull DataPropertyAnnotation annotation,
@@ -646,7 +663,8 @@
     }
 
     // 3a: FieldUseDirectlyWithNullCheck
-    //     Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
+    //     Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
+    //     EmbeddingVector.
     //     We can use this field directly, after testing for null. The java compiler will box
     //     or unbox as needed.
     @NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 8ad865d..dba768a 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -90,6 +90,9 @@
     public static final ClassName GENERIC_DOCUMENT_CLASS =
             ClassName.get(APPSEARCH_PKG, "GenericDocument");
 
+    public static final ClassName EMBEDDING_VECTOR_CLASS =
+            ClassName.get(APPSEARCH_PKG, "EmbeddingVector");
+
     public static final ClassName BUILDER_PRODUCER_CLASS =
             DOCUMENT_ANNOTATION_CLASS.nestedClass("BuilderProducer");
 
@@ -108,6 +111,7 @@
     public final TypeMirror mBooleanPrimitiveType;
     public final TypeMirror mBytePrimitiveArrayType;
     public final TypeMirror mGenericDocumentType;
+    public final TypeMirror mEmbeddingType;
     public final TypeMirror mDoublePrimitiveType;
     final TypeMirror mCollectionType;
     final TypeMirror mListType;
@@ -151,6 +155,8 @@
         mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
         mGenericDocumentType =
                 mElementUtils.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();
+        mEmbeddingType = mElementUtils.getTypeElement(
+                EMBEDDING_VECTOR_CLASS.canonicalName()).asType();
     }
 
     /**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 9199c47..42c6b31 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -28,6 +28,7 @@
 import androidx.annotation.NonNull;
 import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
 import androidx.appsearch.compiler.annotationwrapper.DocumentPropertyAnnotation;
+import androidx.appsearch.compiler.annotationwrapper.EmbeddingPropertyAnnotation;
 import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation;
 import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
 
@@ -229,6 +230,12 @@
                 LongPropertyAnnotation longPropertyAnnotation = (LongPropertyAnnotation) annotation;
                 codeBlock.add(createSetIndexingTypeExpr(longPropertyAnnotation, getterOrField));
                 break;
+            case EMBEDDING_PROPERTY:
+                EmbeddingPropertyAnnotation embeddingPropertyAnnotation =
+                        (EmbeddingPropertyAnnotation) annotation;
+                codeBlock.add(
+                        createSetIndexingTypeExpr(embeddingPropertyAnnotation, getterOrField));
+                break;
             case DOUBLE_PROPERTY: // fall-through
             case BOOLEAN_PROPERTY: // fall-through
             case BYTES_PROPERTY:
@@ -418,6 +425,31 @@
 
     /**
      * Creates an expr like
+     * {@code .setIndexingType(EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)}.
+     */
+    @NonNull
+    private static CodeBlock createSetIndexingTypeExpr(
+            @NonNull EmbeddingPropertyAnnotation annotation,
+            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
+        String enumName;
+        switch (annotation.getIndexingType()) {
+            case 0:
+                enumName = "INDEXING_TYPE_NONE";
+                break;
+            case 1:
+                enumName = "INDEXING_TYPE_SIMILARITY";
+                break;
+            default:
+                throw new ProcessingException(
+                        "Unknown indexing type " + annotation.getIndexingType(),
+                        getterOrField.getElement());
+        }
+        return CodeBlock.of("\n.setIndexingType($T.$N)",
+                EmbeddingPropertyAnnotation.CONFIG_CLASS, enumName);
+    }
+
+    /**
+     * Creates an expr like
      * {@code .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)}.
      */
     @NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index 96bba93..90ebb78 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -165,10 +165,10 @@
         //       care of unboxing and widening where necessary.
         //
         //   1b: CollectionCallToArray
-        //       Collection contains String or GenericDocument.
-        //       We have to convert this into an array of String[] or GenericDocument[], but no
-        //       conversion of the collection elements is needed. We can use Collection#toArray for
-        //       this.
+        //       Collection contains String, GenericDocument or EmbeddingVector.
+        //       We have to convert this into an array of String[], GenericDocument[] or
+        //       EmbeddingVector[], but no conversion of the collection elements is
+        //       needed. We can use Collection#toArray for this.
         //
         //   1c: CollectionForLoopCallToGenericDocument
         //       Collection contains a class which is annotated with @Document.
@@ -188,8 +188,8 @@
         //       unboxing and widening where necessary.
         //
         //   2b: ArrayUseDirectly
-        //       Array is of type String[], long[], double[], boolean[], byte[][] or
-        //       GenericDocument[].
+        //       Array is of type String[], long[], double[], boolean[], byte[][],
+        //       GenericDocument[] or EmbeddingVector[].
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallToGenericDocument
@@ -207,7 +207,8 @@
 
         // Scenario 3: Single valued fields
         //   3a: FieldUseDirectlyWithNullCheck
-        //       Field is of type String, Long, Integer, Double, Float, Boolean.
+        //       Field is of type String, Long, Integer, Double, Float, Boolean or
+        //       EmbeddingVector.
         //       We can use this field directly, after testing for null. The java compiler will box
         //       or unbox as needed.
         //
@@ -375,6 +376,20 @@
                     default:
                         throw new IllegalStateException("Unhandled type-category: " + typeCategory);
                 }
+            case EMBEDDING_PROPERTY:
+                switch (typeCategory) {
+                    case COLLECTION:
+                        // List<EmbeddingVector>: 1b
+                        return collectionCallToArray(annotation, getterOrField);
+                    case ARRAY:
+                        // EmbeddingVector[]: 2b
+                        return arrayUseDirectly(annotation, getterOrField);
+                    case SINGLE:
+                        // EmbeddingVector: 3a
+                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
+                    default:
+                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
+                }
             default:
                 throw new IllegalStateException("Unhandled annotation: " + annotation);
         }
@@ -417,10 +432,10 @@
     }
 
     // 1b: CollectionCallToArray
-    //     Collection contains String or GenericDocument.
-    //     We have to convert this into an array of String[] or GenericDocument[], but no
-    //     conversion of the collection elements is needed. We can use Collection#toArray for
-    //     this.
+    //     Collection contains String, GenericDocument or EmbeddingVector.
+    //     We have to convert this into an array of String[], GenericDocument[] or
+    //     EmbeddingVector[], but no conversion of the collection elements is
+    //     needed. We can use Collection#toArray for this.
     @NonNull
     private CodeBlock collectionCallToArray(
             @NonNull DataPropertyAnnotation annotation,
@@ -536,8 +551,8 @@
     }
 
     // 2b: ArrayUseDirectly
-    //     Array is of type String[], long[], double[], boolean[], byte[][] or
-    //     GenericDocument[].
+    //     Array is of type String[], long[], double[], boolean[], byte[][],
+    //     GenericDocument[] or EmbeddingVector[].
     //     We can directly use this field with no conversion.
     @NonNull
     private CodeBlock arrayUseDirectly(
@@ -613,7 +628,8 @@
     }
 
     // 3a: FieldUseDirectlyWithNullCheck
-    //     Field is of type String, Long, Integer, Double, Float, Boolean.
+    //     Field is of type String, Long, Integer, Double, Float, Boolean or
+    //     EmbeddingVector.
     //     We can use this field directly, after testing for null. The java compiler will box
     //     or unbox as needed.
     @NonNull
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
index 38621c4..1c78f19 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
@@ -38,12 +38,13 @@
  *     <li>{@link DoublePropertyAnnotation}</li>
  *     <li>{@link BooleanPropertyAnnotation}</li>
  *     <li>{@link BytesPropertyAnnotation}</li>
+ *     <li>{@link EmbeddingPropertyAnnotation}</li>
  * </ul>
  */
 public abstract class DataPropertyAnnotation implements PropertyAnnotation {
     public enum Kind {
         STRING_PROPERTY, DOCUMENT_PROPERTY, LONG_PROPERTY, DOUBLE_PROPERTY, BOOLEAN_PROPERTY,
-        BYTES_PROPERTY
+        BYTES_PROPERTY, EMBEDDING_PROPERTY
     }
 
     @NonNull
@@ -103,6 +104,9 @@
             return LongPropertyAnnotation.parse(annotationParams, defaultName);
         } else if (qualifiedClassName.equals(StringPropertyAnnotation.CLASS_NAME.canonicalName())) {
             return StringPropertyAnnotation.parse(annotationParams, defaultName);
+        } else if (qualifiedClassName.equals(
+                EmbeddingPropertyAnnotation.CLASS_NAME.canonicalName())) {
+            return EmbeddingPropertyAnnotation.parse(annotationParams, defaultName);
         }
         return null;
     }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
new file mode 100644
index 0000000..882c737
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.compiler.annotationwrapper;
+
+import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
+import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.compiler.IntrospectionHelper;
+import androidx.appsearch.compiler.ProcessingException;
+
+import com.google.auto.value.AutoValue;
+import com.squareup.javapoet.ClassName;
+
+import java.util.Map;
+
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * An instance of the {@code @Document.EmbeddingProperty} annotation.
+ */
+@AutoValue
+public abstract class EmbeddingPropertyAnnotation extends DataPropertyAnnotation {
+    public static final ClassName CLASS_NAME =
+            DOCUMENT_ANNOTATION_CLASS.nestedClass("EmbeddingProperty");
+
+    public static final ClassName CONFIG_CLASS =
+            APPSEARCH_SCHEMA_CLASS.nestedClass("EmbeddingPropertyConfig");
+
+    public EmbeddingPropertyAnnotation() {
+        super(
+                CLASS_NAME,
+                CONFIG_CLASS,
+                /* genericDocGetterName= */"getPropertyEmbedding",
+                /* genericDocArrayGetterName= */"getPropertyEmbeddingArray",
+                /* genericDocSetterName= */"setPropertyEmbedding");
+    }
+
+    /**
+     * @param defaultName The name to use for the annotated property in case the annotation
+     *                    params do not mention an explicit name.
+     * @throws ProcessingException If the annotation points to an Illegal serializer class.
+     */
+    @NonNull
+    static EmbeddingPropertyAnnotation parse(
+            @NonNull Map<String, Object> annotationParams,
+            @NonNull String defaultName) throws ProcessingException {
+        String name = (String) annotationParams.get("name");
+        return new AutoValue_EmbeddingPropertyAnnotation(
+                name.isEmpty() ? defaultName : name,
+                (boolean) annotationParams.get("required"),
+                (int) annotationParams.get("indexingType"));
+    }
+
+    /**
+     * Specifies how a property should be indexed.
+     */
+    public abstract int getIndexingType();
+
+    @NonNull
+    @Override
+    public final Kind getDataPropertyKind() {
+        return Kind.EMBEDDING_PROPERTY;
+    }
+
+    @NonNull
+    @Override
+    public TypeMirror getUnderlyingTypeWithinGenericDoc(@NonNull IntrospectionHelper helper) {
+        return helper.mEmbeddingType;
+    }
+}
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 57cbd28..fa7b7aa 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -1048,18 +1048,20 @@
         // TODO(b/156296904): Uncomment Gift in this test when it's supported
         Compilation compilation = compile(
                 "import java.util.*;\n"
+                        + "import androidx.appsearch.app.EmbeddingVector;\n"
                         + "@Document\n"
                         + "public class Gift {\n"
                         + "  @Document.Namespace String namespace;\n"
                         + "  @Document.Id String id;\n"
-                        + "  @Document.StringProperty String stringProp;\n"
-                        + "  @Document.LongProperty Integer integerProp;\n"
-                        + "  @Document.LongProperty Long longProp;\n"
-                        + "  @Document.DoubleProperty Float floatProp;\n"
-                        + "  @Document.DoubleProperty Double doubleProp;\n"
-                        + "  @Document.BooleanProperty Boolean booleanProp;\n"
-                        + "  @Document.BytesProperty byte[] bytesProp;\n"
-                        //+ "  @Document.Property Gift documentProp;\n"
+                        + "  @StringProperty String stringProp;\n"
+                        + "  @LongProperty Integer integerProp;\n"
+                        + "  @LongProperty Long longProp;\n"
+                        + "  @DoubleProperty Float floatProp;\n"
+                        + "  @DoubleProperty Double doubleProp;\n"
+                        + "  @BooleanProperty Boolean booleanProp;\n"
+                        + "  @BytesProperty byte[] bytesProp;\n"
+                        + "  @EmbeddingProperty EmbeddingVector vectorProp;\n"
+                        //+ "  @DocumentProperty Gift documentProp;\n"
                         + "}\n");
 
         assertThat(compilation).succeededWithoutWarnings();
@@ -1260,6 +1262,7 @@
         Compilation compilation = compile(
                 "import java.util.*;\n"
                         + "import androidx.appsearch.app.GenericDocument;\n"
+                        + "import androidx.appsearch.app.EmbeddingVector;\n"
                         + "@Document\n"
                         + "public class Gift {\n"
                         + "  @Namespace String namespace;\n"
@@ -1274,6 +1277,7 @@
                         + "  @BytesProperty Collection<byte[]> collectByteArr;\n"    // 1a
                         + "  @StringProperty Collection<String> collectString;\n"     // 1b
                         + "  @DocumentProperty Collection<Gift> collectGift;\n"         // 1c
+                        + "  @EmbeddingProperty Collection<EmbeddingVector> collectVec;\n"   // 1b
                         + "\n"
                         + "  // Arrays\n"
                         + "  @LongProperty Long[] arrBoxLong;\n"         // 2a
@@ -1289,6 +1293,7 @@
                         + "  @BytesProperty byte[][] arrUnboxByteArr;\n"  // 2b
                         + "  @StringProperty String[] arrString;\n"        // 2b
                         + "  @DocumentProperty Gift[] arrGift;\n"            // 2c
+                        + "  @EmbeddingProperty EmbeddingVector[] arrVec;\n"         // 2b
                         + "\n"
                         + "  // Single values\n"
                         + "  @StringProperty String string;\n"        // 3a
@@ -1304,6 +1309,7 @@
                         + "  @BooleanProperty boolean unboxBoolean;\n" // 3b
                         + "  @BytesProperty byte[] unboxByteArr;\n"  // 3a
                         + "  @DocumentProperty Gift gift;\n"            // 3c
+                        + "  @EmbeddingProperty EmbeddingVector vec;\n"        // 3a
                         + "}\n");
 
         assertThat(compilation).succeededWithoutWarnings();
@@ -3480,6 +3486,38 @@
         checkEqualsGolden("Gift.java");
     }
 
+    @Test
+    public void testEmbeddingFields() throws Exception {
+        Compilation compilation = compile(
+                "import java.util.*;\n"
+                        + "import androidx.appsearch.app.EmbeddingVector;\n"
+                        + "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.StringProperty String name;\n"
+                        // Embedding properties
+                        + "  @EmbeddingProperty EmbeddingVector defaultIndexNone;\n"
+                        + "  @EmbeddingProperty(indexingType=0) EmbeddingVector indexNone;\n"
+                        + "  @EmbeddingProperty(indexingType=1) EmbeddingVector vec;\n"
+                        + "  @EmbeddingProperty(indexingType=1) List<EmbeddingVector> listVec;\n"
+                        + "  @EmbeddingProperty(indexingType=1)"
+                        + "  Collection<EmbeddingVector> collectVec;\n"
+                        + "  @EmbeddingProperty(indexingType=1) EmbeddingVector[] arrVec;\n"
+                        + "}\n");
+
+        assertThat(compilation).succeededWithoutWarnings();
+        checkResultContains("Gift.java",
+                "new AppSearchSchema.EmbeddingPropertyConfig.Builder");
+        checkResultContains("Gift.java",
+                "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY");
+        checkResultContains("Gift.java",
+                "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE");
+        checkResultContains("Gift.java",
+                "EmbeddingVector");
+        checkEqualsGolden("Gift.java");
+    }
+
     private Compilation compile(String classBody) {
         return compile("Gift", classBody, /* restrictGeneratedCodeToLibrary= */false);
     }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index 4d65cc9..387d97d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -55,6 +56,10 @@
           .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytesProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
             .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vectorProp")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
           .build();
   }
 
@@ -95,6 +100,10 @@
     if (bytesPropCopy != null) {
       builder.setPropertyBytes("bytesProp", bytesPropCopy);
     }
+    EmbeddingVector vectorPropCopy = document.vectorProp;
+    if (vectorPropCopy != null) {
+      builder.setPropertyEmbedding("vectorProp", vectorPropCopy);
+    }
     return builder.build();
   }
 
@@ -138,6 +147,11 @@
     if (bytesPropCopy != null && bytesPropCopy.length != 0) {
       bytesPropConv = bytesPropCopy[0];
     }
+    EmbeddingVector[] vectorPropCopy = genericDoc.getPropertyEmbeddingArray("vectorProp");
+    EmbeddingVector vectorPropConv = null;
+    if (vectorPropCopy != null && vectorPropCopy.length != 0) {
+      vectorPropConv = vectorPropCopy[0];
+    }
     Gift document = new Gift();
     document.namespace = namespaceConv;
     document.id = idConv;
@@ -148,6 +162,7 @@
     document.doubleProp = doublePropConv;
     document.booleanProp = booleanPropConv;
     document.bytesProp = bytesPropConv;
+    document.vectorProp = vectorPropConv;
     return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
new file mode 100644
index 0000000..9447804
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
@@ -0,0 +1,153 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
+
+  @Override
+  public String getSchemaName() {
+    return SCHEMA_NAME;
+  }
+
+  @Override
+  public AppSearchSchema getSchema() throws AppSearchException {
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("name")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+            .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("defaultIndexNone")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("indexNone")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("listVec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+            .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+            .build())
+          .build();
+  }
+
+  @Override
+  public List<Class<?>> getDependencyDocumentClasses() throws AppSearchException {
+    return Collections.emptyList();
+  }
+
+  @Override
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+    GenericDocument.Builder<?> builder =
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String nameCopy = document.name;
+    if (nameCopy != null) {
+      builder.setPropertyString("name", nameCopy);
+    }
+    EmbeddingVector defaultIndexNoneCopy = document.defaultIndexNone;
+    if (defaultIndexNoneCopy != null) {
+      builder.setPropertyEmbedding("defaultIndexNone", defaultIndexNoneCopy);
+    }
+    EmbeddingVector indexNoneCopy = document.indexNone;
+    if (indexNoneCopy != null) {
+      builder.setPropertyEmbedding("indexNone", indexNoneCopy);
+    }
+    EmbeddingVector vecCopy = document.vec;
+    if (vecCopy != null) {
+      builder.setPropertyEmbedding("vec", vecCopy);
+    }
+    List<EmbeddingVector> listVecCopy = document.listVec;
+    if (listVecCopy != null) {
+      EmbeddingVector[] listVecConv = listVecCopy.toArray(new EmbeddingVector[0]);
+      builder.setPropertyEmbedding("listVec", listVecConv);
+    }
+    Collection<EmbeddingVector> collectVecCopy = document.collectVec;
+    if (collectVecCopy != null) {
+      EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
+      builder.setPropertyEmbedding("collectVec", collectVecConv);
+    }
+    EmbeddingVector[] arrVecCopy = document.arrVec;
+    if (arrVecCopy != null) {
+      builder.setPropertyEmbedding("arrVec", arrVecCopy);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public Gift fromGenericDocument(GenericDocument genericDoc,
+      Map<String, List<String>> documentClassMap) throws AppSearchException {
+    String namespaceConv = genericDoc.getNamespace();
+    String idConv = genericDoc.getId();
+    String[] nameCopy = genericDoc.getPropertyStringArray("name");
+    String nameConv = null;
+    if (nameCopy != null && nameCopy.length != 0) {
+      nameConv = nameCopy[0];
+    }
+    EmbeddingVector[] defaultIndexNoneCopy = genericDoc.getPropertyEmbeddingArray("defaultIndexNone");
+    EmbeddingVector defaultIndexNoneConv = null;
+    if (defaultIndexNoneCopy != null && defaultIndexNoneCopy.length != 0) {
+      defaultIndexNoneConv = defaultIndexNoneCopy[0];
+    }
+    EmbeddingVector[] indexNoneCopy = genericDoc.getPropertyEmbeddingArray("indexNone");
+    EmbeddingVector indexNoneConv = null;
+    if (indexNoneCopy != null && indexNoneCopy.length != 0) {
+      indexNoneConv = indexNoneCopy[0];
+    }
+    EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
+    EmbeddingVector vecConv = null;
+    if (vecCopy != null && vecCopy.length != 0) {
+      vecConv = vecCopy[0];
+    }
+    EmbeddingVector[] listVecCopy = genericDoc.getPropertyEmbeddingArray("listVec");
+    List<EmbeddingVector> listVecConv = null;
+    if (listVecCopy != null) {
+      listVecConv = Arrays.asList(listVecCopy);
+    }
+    EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
+    List<EmbeddingVector> collectVecConv = null;
+    if (collectVecCopy != null) {
+      collectVecConv = Arrays.asList(collectVecCopy);
+    }
+    EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.name = nameConv;
+    document.defaultIndexNone = defaultIndexNoneConv;
+    document.indexNone = indexNoneConv;
+    document.vec = vecConv;
+    document.listVec = listVecConv;
+    document.collectVec = collectVecConv;
+    document.arrVec = arrVecConv;
+    return document;
+  }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index ee36e9d6..6ac5c27 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -2,6 +2,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -61,6 +62,10 @@
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
             .setShouldIndexNestedProperties(false)
             .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
           .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrBoxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
             .setIndexingType(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE)
@@ -108,6 +113,10 @@
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
             .setShouldIndexNestedProperties(false)
             .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
           .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("string")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
             .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
@@ -155,6 +164,10 @@
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
             .setShouldIndexNestedProperties(false)
             .build())
+          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
           .build();
   }
 
@@ -237,6 +250,11 @@
       }
       builder.setPropertyDocument("collectGift", collectGiftConv);
     }
+    Collection<EmbeddingVector> collectVecCopy = document.collectVec;
+    if (collectVecCopy != null) {
+      EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
+      builder.setPropertyEmbedding("collectVec", collectVecConv);
+    }
     Long[] arrBoxLongCopy = document.arrBoxLong;
     if (arrBoxLongCopy != null) {
       long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
@@ -321,6 +339,10 @@
       }
       builder.setPropertyDocument("arrGift", arrGiftConv);
     }
+    EmbeddingVector[] arrVecCopy = document.arrVec;
+    if (arrVecCopy != null) {
+      builder.setPropertyEmbedding("arrVec", arrVecCopy);
+    }
     String stringCopy = document.string;
     if (stringCopy != null) {
       builder.setPropertyString("string", stringCopy);
@@ -359,6 +381,10 @@
       GenericDocument giftConv = GenericDocument.fromDocumentClass(giftCopy);
       builder.setPropertyDocument("gift", giftConv);
     }
+    EmbeddingVector vecCopy = document.vec;
+    if (vecCopy != null) {
+      builder.setPropertyEmbedding("vec", vecCopy);
+    }
     return builder.build();
   }
 
@@ -428,6 +454,11 @@
         collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class, documentClassMap));
       }
     }
+    EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
+    List<EmbeddingVector> collectVecConv = null;
+    if (collectVecCopy != null) {
+      collectVecConv = Arrays.asList(collectVecCopy);
+    }
     long[] arrBoxLongCopy = genericDoc.getPropertyLongArray("arrBoxLong");
     Long[] arrBoxLongConv = null;
     if (arrBoxLongCopy != null) {
@@ -497,6 +528,7 @@
         arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class, documentClassMap);
       }
     }
+    EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
     String[] stringCopy = genericDoc.getPropertyStringArray("string");
     String stringConv = null;
     if (stringCopy != null && stringCopy.length != 0) {
@@ -542,6 +574,11 @@
     if (giftCopy != null) {
       giftConv = giftCopy.toDocumentClass(Gift.class, documentClassMap);
     }
+    EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
+    EmbeddingVector vecConv = null;
+    if (vecCopy != null && vecCopy.length != 0) {
+      vecConv = vecCopy[0];
+    }
     Gift document = new Gift();
     document.namespace = namespaceConv;
     document.id = idConv;
@@ -553,6 +590,7 @@
     document.collectByteArr = collectByteArrConv;
     document.collectString = collectStringConv;
     document.collectGift = collectGiftConv;
+    document.collectVec = collectVecConv;
     document.arrBoxLong = arrBoxLongConv;
     document.arrUnboxLong = arrUnboxLongConv;
     document.arrBoxInteger = arrBoxIntegerConv;
@@ -566,6 +604,7 @@
     document.arrUnboxByteArr = arrUnboxByteArrConv;
     document.arrString = arrStringConv;
     document.arrGift = arrGiftConv;
+    document.arrVec = arrVecConv;
     document.string = stringConv;
     document.boxLong = boxLongConv;
     document.unboxLong = unboxLongConv;
@@ -579,6 +618,7 @@
     document.unboxBoolean = unboxBooleanConv;
     document.unboxByteArr = unboxByteArrConv;
     document.gift = giftConv;
+    document.vec = vecConv;
     return document;
   }
 }
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 8b3a414..b483b72 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -64,7 +64,8 @@
 FRAMEWORK_API_TEST_ROOT = 'testing/coretests/src/android/app/appsearch/external'
 FRAMEWORK_IMPL_ROOT = 'service/java/com/android/server/appsearch/external'
 FRAMEWORK_IMPL_TEST_ROOT = 'testing/servicestests/src/com/android/server/appsearch/external'
-FRAMEWORK_TEST_UTIL_ROOT = '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external'
+FRAMEWORK_TEST_UTIL_ROOT = (
+    '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external')
 FRAMEWORK_TEST_UTIL_TEST_ROOT = 'testing/servicestests/src/android/app/appsearch/testutil/external'
 FRAMEWORK_CTS_TEST_ROOT = '../../../cts/tests/appsearch/src/com/android/cts/appsearch/external'
 GOOGLE_JAVA_FORMAT = (
@@ -88,19 +89,34 @@
                 os.remove(abs_path)
 
     def _TransformAndCopyFile(
-            self, source_path, dest_path, transform_func=None, ignore_skips=False):
+            self, source_path, default_dest_path, transform_func=None, ignore_skips=False):
+        """
+        Transforms the file located at 'source_path' and writes it into 'default_dest_path'.
+
+        An @exportToFramework:skip() directive will skip the copy process.
+        An @exportToFramework:copyToPath() directive will override default_dest_path with another
+          path relative to framework_appsearch_root (which is usually packages/modules/AppSearch)
+        """
         with open(source_path, 'r') as fh:
             contents = fh.read()
 
         if not ignore_skips and '@exportToFramework:skipFile()' in contents:
-            print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
+            print('Skipping: "%s" -> "%s"' % (source_path, default_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))
+        copy_to_path = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
+        if copy_to_path:
+            dest_path = os.path.join(self._framework_appsearch_root, copy_to_path.group(1))
+        else:
+            dest_path = default_dest_path
 
+        self._TransformAndCopyFileToPath(source_path, dest_path, transform_func)
+
+    def _TransformAndCopyFileToPath(self, source_path, dest_path, transform_func=None):
+        """Transforms the file located at 'source_path' and writes it into 'dest_path'."""
         print('Copy: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
+        with open(source_path, 'r') as fh:
+            contents = fh.read()
         if transform_func:
             contents = transform_func(contents)
         os.makedirs(os.path.dirname(dest_path), exist_ok=True)
@@ -149,6 +165,8 @@
             .replace(
                     'androidx.appsearch.localstorage.',
                     'com.android.server.appsearch.external.localstorage.')
+            .replace('androidx.appsearch.flags.FlaggedApi', 'android.annotation.FlaggedApi')
+            .replace('androidx.appsearch.flags.Flags', 'com.android.appsearch.flags.Flags')
             .replace('androidx.appsearch', 'android.app.appsearch')
             .replace(
                     'androidx.annotation.GuardedBy',
@@ -180,6 +198,7 @@
             .replace('// @exportToFramework:skipFile()', '')
         )
         contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
+        contents = re.sub(r'@RequiresFeature\([^)]*\)', '', contents, flags=re.DOTALL)
 
         # Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
         # to allow the same documentation to compile for both.
@@ -190,16 +209,27 @@
 
     def _TransformTestCode(self, contents):
         contents = (contents
+            .replace(
+                    'androidx.appsearch.flags.CheckFlagsRule',
+                    'android.platform.test.flag.junit.CheckFlagsRule')
+            .replace(
+                    'androidx.appsearch.flags.DeviceFlagsValueProvider',
+                    'android.platform.test.flag.junit.DeviceFlagsValueProvider')
+            .replace(
+                    'androidx.appsearch.flags.RequiresFlagsEnabled',
+                    'android.platform.test.annotations.RequiresFlagsEnabled')
             .replace('androidx.appsearch.testutil.', 'android.app.appsearch.testutil.')
             .replace(
                     'package androidx.appsearch.testutil;',
                     'package android.app.appsearch.testutil;')
             .replace(
-                    'androidx.appsearch.localstorage.LocalStorage',
-                    'android.app.appsearch.AppSearchManager')
+                    'import androidx.appsearch.localstorage.LocalStorage;',
+                    'import android.app.appsearch.AppSearchManager;')
             .replace('LocalStorage.', 'AppSearchManager.')
         )
-        for shim in ['AppSearchSession', 'GlobalSearchSession', 'SearchResults']:
+        for shim in [
+                'AppSearchSession', 'GlobalSearchSession', 'EnterpriseGlobalSearchSession',
+                'SearchResults']:
             contents = re.sub(r"([^a-zA-Z])(%s)([^a-zA-Z0-9])" % shim, r'\1\2Shim\3', contents)
         return self._TransformCommonCode(contents)
 
@@ -277,7 +307,8 @@
         self._TransformAndCopyFolder(
                 test_util_source_dir, test_util_dest_dir, transform_func=self._TransformTestCode)
         for iface_file in (
-                'AppSearchSession.java', 'GlobalSearchSession.java', 'SearchResults.java'):
+                'AppSearchSession.java', 'GlobalSearchSession.java',
+                'EnterpriseGlobalSearchSession.java', 'SearchResults.java'):
             dest_file_name = os.path.splitext(iface_file)[0] + 'Shim.java'
             self._TransformAndCopyFile(
                     os.path.join(api_source_dir, 'app/' + iface_file),
@@ -318,6 +349,8 @@
                             'package com.android.server.appsearch.external')
                     .replace('com.google.android.icing.proto.',
                             'com.android.server.appsearch.icing.proto.')
+                    .replace('com.google.android.appsearch.proto.',
+                            'com.android.server.appsearch.appsearch.proto.')
                     .replace('com.google.android.icing.protobuf.',
                             'com.android.server.appsearch.protobuf.')
             )
@@ -360,6 +393,32 @@
         print('Wrote "%s"' % file_path)
         return old_sha
 
+    def FormatCommitMessage(self, old_sha, new_sha):
+        print('\nCommand to diff old version to new version:')
+        print('  git log --pretty=format:"* %h %s" {}..{} -- appsearch/'.format(old_sha, new_sha))
+        pretty_log = subprocess.check_output([
+            'git',
+            'log',
+            '--pretty=format:* %h %s',
+            '{}..{}'.format(old_sha, new_sha),
+            '--',
+            'appsearch/'
+        ]).decode("utf-8")
+        bug_output = subprocess.check_output([
+            '/bin/sh',
+            '-c',
+            'git log {}..{} -- appsearch/ | grep Bug: | sort | uniq'.format(old_sha, new_sha)
+        ]).decode("utf-8")
+
+        print('\n--------------------------------------------------')
+        print('Update Framework from Jetpack.\n')
+        print(pretty_log)
+        print()
+        for line in bug_output.splitlines():
+            print(line.strip())
+        print('Test: Presubmit\n')
+        print('--------------------------------------------------\n')
+
 
 if __name__ == '__main__':
     if len(sys.argv) != 3:
@@ -388,5 +447,4 @@
     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))
+        exporter.FormatCommitMessage(old_sha, new_sha)
diff --git a/benchmark/benchmark-common/api/1.3.0-beta01.txt b/benchmark/benchmark-common/api/1.3.0-beta01.txt
deleted file mode 100644
index 9df6fae..0000000
--- a/benchmark/benchmark-common/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,139 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark {
-
-  public final class BenchmarkState {
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
-    method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
-    method public boolean keepRunning();
-    method public void pauseTiming();
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-    method public void resumeTiming();
-    field public static final androidx.benchmark.BenchmarkState.Companion Companion;
-  }
-
-  public static final class BenchmarkState.Companion {
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
-    method public static void consume(boolean value);
-    method public static void consume(byte value);
-    method public static void consume(char value);
-    method public static void consume(double value);
-    method public static void consume(float value);
-    method public static void consume(int value);
-    method public static void consume(Object value);
-    method public static void consume(long value);
-    method public static void consume(short value);
-    field public static final androidx.benchmark.BlackHole INSTANCE;
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkConfigApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBlackHoleApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract class MetricCapture {
-    ctor public MetricCapture(java.util.List<java.lang.String> names);
-    method public abstract void capturePaused();
-    method public abstract void captureResumed();
-    method public abstract void captureStart(long timeNs);
-    method public abstract void captureStop(long timeNs, long[] output, int offset);
-    method public final java.util.List<java.lang.String> getNames();
-    property public final java.util.List<java.lang.String> names;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
-    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
-    method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
-    method public androidx.benchmark.ProfilerConfig? getProfiler();
-    method public boolean isPerfettoSdkTracingEnabled();
-    method public boolean isTraceAppTagEnabled();
-    property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
-    property public final boolean perfettoSdkTracingEnabled;
-    property public final androidx.benchmark.ProfilerConfig? profiler;
-    property public final boolean traceAppTagEnabled;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
-  }
-
-  public static final class ProfilerConfig.MethodTracing extends androidx.benchmark.ProfilerConfig {
-    ctor public ProfilerConfig.MethodTracing();
-    field public static final boolean AFFECTS_MEASUREMENTS_ON_THIS_DEVICE;
-    field public static final androidx.benchmark.ProfilerConfig.MethodTracing.Companion Companion;
-  }
-
-  public static final class ProfilerConfig.MethodTracing.Companion {
-  }
-
-  public static final class ProfilerConfig.StackSampling extends androidx.benchmark.ProfilerConfig {
-    ctor public ProfilerConfig.StackSampling();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class TimeCapture extends androidx.benchmark.MetricCapture {
-    ctor public TimeCapture();
-    ctor public TimeCapture(optional String name);
-    method public void capturePaused();
-    method public void captureResumed();
-    method public void captureStart(long timeNs);
-    method public void captureStop(long timeNs, long[] output, int offset);
-  }
-
-}
-
-package androidx.benchmark.perfetto {
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public abstract sealed class PerfettoConfig {
-  }
-
-  public static final class PerfettoConfig.Binary extends androidx.benchmark.perfetto.PerfettoConfig {
-    ctor public PerfettoConfig.Binary(byte[] bytes);
-    method public byte[] getBytes();
-    property public final byte[] bytes;
-  }
-
-  public static final class PerfettoConfig.Text extends androidx.benchmark.perfetto.PerfettoConfig {
-    ctor public PerfettoConfig.Text(String text);
-    method public String getText();
-    property public final String text;
-  }
-
-  @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
-    ctor public PerfettoTrace(String path);
-    method public String getPath();
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    property public final String path;
-    field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
-  }
-
-  public static final class PerfettoTrace.Companion {
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-  }
-
-}
-
diff --git a/benchmark/benchmark-common/api/res-1.3.0-beta01.txt b/benchmark/benchmark-common/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-common/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e2bfb40..0000000
--- a/benchmark/benchmark-common/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,141 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark {
-
-  public final class BenchmarkState {
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public BenchmarkState(optional Integer? warmupCount, optional Integer? repeatCount);
-    method @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkStateApi public java.util.List<java.lang.Double> getMeasurementTimeNs();
-    method public boolean keepRunning();
-    method @kotlin.PublishedApi internal boolean keepRunningInternal();
-    method public void pauseTiming();
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public static void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-    method public void resumeTiming();
-    field public static final androidx.benchmark.BenchmarkState.Companion Companion;
-    field @kotlin.PublishedApi internal int iterationsRemaining;
-  }
-
-  public static final class BenchmarkState.Companion {
-    method @SuppressCompatibility @androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport public void reportData(String className, String testName, @IntRange(from=0L) long totalRunTimeNs, java.util.List<java.lang.Long> dataNs, @IntRange(from=0L) int warmupIterations, @IntRange(from=0L) long thermalThrottleSleepSeconds, @IntRange(from=1L) int repeatIterations);
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public static @interface BenchmarkState.Companion.ExperimentalExternalReport {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBlackHoleApi public final class BlackHole {
-    method public static void consume(boolean value);
-    method public static void consume(byte value);
-    method public static void consume(char value);
-    method public static void consume(double value);
-    method public static void consume(float value);
-    method public static void consume(int value);
-    method public static void consume(Object value);
-    method public static void consume(long value);
-    method public static void consume(short value);
-    field public static final androidx.benchmark.BlackHole INSTANCE;
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkConfigApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBlackHoleApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract class MetricCapture {
-    ctor public MetricCapture(java.util.List<java.lang.String> names);
-    method public abstract void capturePaused();
-    method public abstract void captureResumed();
-    method public abstract void captureStart(long timeNs);
-    method public abstract void captureStop(long timeNs, long[] output, int offset);
-    method public final java.util.List<java.lang.String> getNames();
-    property public final java.util.List<java.lang.String> names;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class MicrobenchmarkConfig {
-    ctor public MicrobenchmarkConfig(optional java.util.List<? extends androidx.benchmark.MetricCapture> metrics, optional boolean traceAppTagEnabled, optional boolean perfettoSdkTracingEnabled, optional androidx.benchmark.ProfilerConfig? profiler);
-    method public java.util.List<androidx.benchmark.MetricCapture> getMetrics();
-    method public androidx.benchmark.ProfilerConfig? getProfiler();
-    method public boolean isPerfettoSdkTracingEnabled();
-    method public boolean isTraceAppTagEnabled();
-    property public final java.util.List<androidx.benchmark.MetricCapture> metrics;
-    property public final boolean perfettoSdkTracingEnabled;
-    property public final androidx.benchmark.ProfilerConfig? profiler;
-    property public final boolean traceAppTagEnabled;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public abstract sealed class ProfilerConfig {
-  }
-
-  public static final class ProfilerConfig.MethodTracing extends androidx.benchmark.ProfilerConfig {
-    ctor public ProfilerConfig.MethodTracing();
-    field public static final boolean AFFECTS_MEASUREMENTS_ON_THIS_DEVICE;
-    field public static final androidx.benchmark.ProfilerConfig.MethodTracing.Companion Companion;
-  }
-
-  public static final class ProfilerConfig.MethodTracing.Companion {
-  }
-
-  public static final class ProfilerConfig.StackSampling extends androidx.benchmark.ProfilerConfig {
-    ctor public ProfilerConfig.StackSampling();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public final class TimeCapture extends androidx.benchmark.MetricCapture {
-    ctor public TimeCapture();
-    ctor public TimeCapture(optional String name);
-    method public void capturePaused();
-    method public void captureResumed();
-    method public void captureStart(long timeNs);
-    method public void captureStop(long timeNs, long[] output, int offset);
-  }
-
-}
-
-package androidx.benchmark.perfetto {
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public abstract sealed class PerfettoConfig {
-  }
-
-  public static final class PerfettoConfig.Binary extends androidx.benchmark.perfetto.PerfettoConfig {
-    ctor public PerfettoConfig.Binary(byte[] bytes);
-    method public byte[] getBytes();
-    property public final byte[] bytes;
-  }
-
-  public static final class PerfettoConfig.Text extends androidx.benchmark.perfetto.PerfettoConfig {
-    ctor public PerfettoConfig.Text(String text);
-    method public String getText();
-    property public final String text;
-  }
-
-  @SuppressCompatibility @RequiresApi(23) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
-    ctor public PerfettoTrace(String path);
-    method public String getPath();
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    property public final String path;
-    field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
-  }
-
-  public static final class PerfettoTrace.Companion {
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, optional String highlightPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, androidx.benchmark.perfetto.PerfettoConfig config, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
-  }
-
-}
-
diff --git a/benchmark/benchmark-junit4/api/1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/1.3.0-beta01.txt
deleted file mode 100644
index fc4b392..0000000
--- a/benchmark/benchmark-junit4/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,36 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.junit4 {
-
-  public class AndroidBenchmarkRunner extends androidx.test.runner.AndroidJUnitRunner {
-    ctor public AndroidBenchmarkRunner();
-  }
-
-  public final class BenchmarkRule implements org.junit.rules.TestRule {
-    ctor public BenchmarkRule();
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public androidx.benchmark.BenchmarkState getState();
-  }
-
-  public final class BenchmarkRule.Scope {
-    method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
-  }
-
-  public final class BenchmarkRuleKt {
-    method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
-    method public static inline void measureRepeatedOnMainThread(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
-    ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public boolean getEnableAppTagTracing();
-    method public boolean getEnableUserspaceTracing();
-    method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
-    property public final boolean enableAppTagTracing;
-    property public final boolean enableUserspaceTracing;
-    property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
-  }
-
-}
-
diff --git a/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-junit4/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index 6aac49d..0000000
--- a/benchmark/benchmark-junit4/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.junit4 {
-
-  public class AndroidBenchmarkRunner extends androidx.test.runner.AndroidJUnitRunner {
-    ctor public AndroidBenchmarkRunner();
-  }
-
-  public final class BenchmarkRule implements org.junit.rules.TestRule {
-    ctor public BenchmarkRule();
-    ctor @SuppressCompatibility @androidx.benchmark.ExperimentalBenchmarkConfigApi public BenchmarkRule(androidx.benchmark.MicrobenchmarkConfig config);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public androidx.benchmark.BenchmarkState getState();
-  }
-
-  public final class BenchmarkRule.Scope {
-    method @kotlin.PublishedApi internal androidx.benchmark.BenchmarkState getOuterState();
-    method public inline <T> T runWithTimingDisabled(kotlin.jvm.functions.Function0<? extends T> block);
-  }
-
-  public final class BenchmarkRuleKt {
-    method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
-    method public static inline void measureRepeatedOnMainThread(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
-    ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public boolean getEnableAppTagTracing();
-    method public boolean getEnableUserspaceTracing();
-    method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
-    property public final boolean enableAppTagTracing;
-    property public final boolean enableUserspaceTracing;
-    property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
-  }
-
-}
-
diff --git a/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt
deleted file mode 100644
index e06bf57..0000000
--- a/benchmark/benchmark-macro-junit4/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro.junit4 {
-
-  @RequiresApi(28) public final class BaselineProfileRule implements org.junit.rules.TestRule {
-    ctor public BaselineProfileRule();
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, optional kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> filterPredicate, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-  }
-
-  public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
-    ctor public MacrobenchmarkRule();
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-  }
-
-}
-
diff --git a/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-macro-junit4/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e06bf57..0000000
--- a/benchmark/benchmark-macro-junit4/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro.junit4 {
-
-  @RequiresApi(28) public final class BaselineProfileRule implements org.junit.rules.TestRule {
-    ctor public BaselineProfileRule();
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, optional boolean strictStability, optional kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> filterPredicate, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, optional boolean includeInStartupProfile, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, optional String? outputFilePrefix, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, optional int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-    method public void collect(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
-  }
-
-  public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
-    ctor public MacrobenchmarkRule();
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-    method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
-  }
-
-}
-
diff --git a/benchmark/benchmark-macro/api/1.3.0-beta01.txt b/benchmark/benchmark-macro/api/1.3.0-beta01.txt
deleted file mode 100644
index ad9fb40..0000000
--- a/benchmark/benchmark-macro/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,281 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro {
-
-  public enum BaselineProfileMode {
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
-  }
-
-  public abstract sealed class CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
-    field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
-  }
-
-  public static final class CompilationMode.Companion {
-  }
-
-  public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Full();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMacrobenchmarkApi public static final class CompilationMode.Ignore extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Ignore();
-  }
-
-  @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.None();
-  }
-
-  @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Partial();
-    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
-    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0L) int warmupIterations);
-    method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
-    method public int getWarmupIterations();
-    property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
-    property public final int warmupIterations;
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn(message="This Macrobenchmark API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMacrobenchmarkApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
-    ctor public FrameTimingGfxInfoMetric();
-  }
-
-  public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
-    ctor public FrameTimingMetric();
-  }
-
-  public final class MacrobenchmarkScope {
-    ctor public MacrobenchmarkScope(String packageName, boolean launchWithClearTask);
-    method public void dropKernelPageCache();
-    method public void dropShaderCache();
-    method public androidx.test.uiautomator.UiDevice getDevice();
-    method public Integer? getIteration();
-    method public String getPackageName();
-    method public void killProcess();
-    method @Deprecated public void killProcess(optional boolean useKillAll);
-    method public void pressHome();
-    method public void pressHome(optional long delayDurationMs);
-    method public void startActivityAndWait();
-    method public void startActivityAndWait(android.content.Intent intent);
-    method public void startActivityAndWait(optional kotlin.jvm.functions.Function1<? super android.content.Intent,kotlin.Unit> block);
-    property public final androidx.test.uiautomator.UiDevice device;
-    property public final Integer? iteration;
-    property public final String packageName;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
-    ctor public MemoryCountersMetric();
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
-    ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  public enum MemoryUsageMetric.Mode {
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Last;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Max;
-  }
-
-  public enum MemoryUsageMetric.SubMetric {
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric Gpu;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric HeapSize;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssAnon;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssFile;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssShmem;
-  }
-
-  public abstract sealed class Metric {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.CaptureInfo {
-    ctor public Metric.CaptureInfo(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
-    method public int component1();
-    method public String component2();
-    method public String component3();
-    method public androidx.benchmark.macro.StartupMode? component4();
-    method public androidx.benchmark.macro.Metric.CaptureInfo copy(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
-    method public int getApiLevel();
-    method public androidx.benchmark.macro.StartupMode? getStartupMode();
-    method public String getTargetPackageName();
-    method public String getTestPackageName();
-    property public final int apiLevel;
-    property public final androidx.benchmark.macro.StartupMode? startupMode;
-    property public final String targetPackageName;
-    property public final String testPackageName;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.Measurement {
-    ctor public Metric.Measurement(String name, double data);
-    ctor public Metric.Measurement(String name, java.util.List<java.lang.Double> dataSamples);
-    method public String component1();
-    method public java.util.List<java.lang.Double> component2();
-    method public boolean component3();
-    method public androidx.benchmark.macro.Metric.Measurement copy(String name, java.util.List<java.lang.Double> data, boolean requireSingleValue);
-    method public java.util.List<java.lang.Double> getData();
-    method public String getName();
-    method public boolean getRequireSingleValue();
-    property public final java.util.List<java.lang.Double> data;
-    property public final String name;
-    property public final boolean requireSingleValue;
-  }
-
-  public final class MetricResultExtensionsKt {
-    method @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static void assertEqualMeasurements(java.util.List<androidx.benchmark.macro.Metric.Measurement> expected, java.util.List<androidx.benchmark.macro.Metric.Measurement> observed, double threshold);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategory {
-    enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategoryDisplayLevel {
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
-  }
-
-  @SuppressCompatibility @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class PowerMetric extends androidx.benchmark.macro.Metric {
-    ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
-    method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
-    method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public static boolean deviceBatteryHasMinimumCharge();
-    method public static boolean deviceSupportsHighPrecisionTracking();
-    field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
-  }
-
-  public static final class PowerMetric.Companion {
-    method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
-    method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public boolean deviceBatteryHasMinimumCharge();
-    method public boolean deviceSupportsHighPrecisionTracking();
-  }
-
-  public abstract static sealed class PowerMetric.Type {
-    method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
-    method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
-    property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
-  }
-
-  public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Battery();
-  }
-
-  public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
-  }
-
-  public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
-  }
-
-  public enum StartupMode {
-    enum_constant public static final androidx.benchmark.macro.StartupMode COLD;
-    enum_constant public static final androidx.benchmark.macro.StartupMode HOT;
-    enum_constant public static final androidx.benchmark.macro.StartupMode WARM;
-  }
-
-  public final class StartupTimingMetric extends androidx.benchmark.macro.Metric {
-    ctor public StartupTimingMetric();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
-    ctor public TraceMetric();
-    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
-    ctor public TraceSectionMetric(String sectionName);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
-  }
-
-  public abstract static sealed class TraceSectionMetric.Mode {
-  }
-
-  public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
-  }
-
-}
-
-package androidx.benchmark.perfetto {
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
-    ctor public PerfettoTraceProcessor();
-    method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
-    method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
-  }
-
-  public static final class PerfettoTraceProcessor.Companion {
-    method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-  }
-
-  public static final class PerfettoTraceProcessor.Session {
-    method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
-    method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
-    method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
-    method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
-    method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
-    ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
-    method public byte[] bytes(String columnName);
-    method public double double(String columnName);
-    method public long long(String columnName);
-    method public byte[]? nullableBytes(String columnName);
-    method public Double? nullableDouble(String columnName);
-    method public Long? nullableLong(String columnName);
-    method public String? nullableString(String columnName);
-    method public String string(String columnName);
-  }
-
-  public final class RowKt {
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
-  }
-
-}
-
diff --git a/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt b/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/benchmark/benchmark-macro/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt b/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index 4c883fb..0000000
--- a/benchmark/benchmark-macro/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,303 +0,0 @@
-// Signature format: 4.0
-package androidx.benchmark.macro {
-
-  public enum BaselineProfileMode {
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
-    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BatteryCharge {
-    method public boolean hasMinimumCharge(optional boolean throwOnMissingMetrics);
-    field public static final androidx.benchmark.macro.BatteryCharge INSTANCE;
-  }
-
-  public abstract sealed class CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
-    field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
-  }
-
-  public static final class CompilationMode.Companion {
-  }
-
-  public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Full();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMacrobenchmarkApi public static final class CompilationMode.Ignore extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Ignore();
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class CompilationMode.Interpreted extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Interpreted INSTANCE;
-  }
-
-  @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.None();
-  }
-
-  @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.Partial();
-    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
-    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0L) int warmupIterations);
-    method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
-    method public int getWarmupIterations();
-    property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
-    property public final int warmupIterations;
-  }
-
-  public final class CompilationModeKt {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static boolean isSupportedWithVmSettings(androidx.benchmark.macro.CompilationMode);
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn(message="This Macrobenchmark API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMacrobenchmarkApi {
-  }
-
-  @SuppressCompatibility @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class FrameTimingGfxInfoMetric extends androidx.benchmark.macro.Metric {
-    ctor public FrameTimingGfxInfoMetric();
-  }
-
-  public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
-    ctor public FrameTimingMetric();
-  }
-
-  public final class MacrobenchmarkScope {
-    ctor public MacrobenchmarkScope(String packageName, boolean launchWithClearTask);
-    method public void dropKernelPageCache();
-    method public void dropShaderCache();
-    method public androidx.test.uiautomator.UiDevice getDevice();
-    method public Integer? getIteration();
-    method public String getPackageName();
-    method public void killProcess();
-    method @Deprecated public void killProcess(optional boolean useKillAll);
-    method public void pressHome();
-    method public void pressHome(optional long delayDurationMs);
-    method public void startActivityAndWait();
-    method public void startActivityAndWait(android.content.Intent intent);
-    method public void startActivityAndWait(optional kotlin.jvm.functions.Function1<? super android.content.Intent,kotlin.Unit> block);
-    property public final androidx.test.uiautomator.UiDevice device;
-    property public final Integer? iteration;
-    property public final String packageName;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryCountersMetric extends androidx.benchmark.macro.TraceMetric {
-    ctor public MemoryCountersMetric();
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class MemoryUsageMetric extends androidx.benchmark.macro.TraceMetric {
-    ctor public MemoryUsageMetric(androidx.benchmark.macro.MemoryUsageMetric.Mode mode, optional java.util.List<? extends androidx.benchmark.macro.MemoryUsageMetric.SubMetric> subMetrics);
-    method public java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  public enum MemoryUsageMetric.Mode {
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Last;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.Mode Max;
-  }
-
-  public enum MemoryUsageMetric.SubMetric {
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric Gpu;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric HeapSize;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssAnon;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssFile;
-    enum_constant public static final androidx.benchmark.macro.MemoryUsageMetric.SubMetric RssShmem;
-  }
-
-  public abstract sealed class Metric {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.CaptureInfo {
-    ctor public Metric.CaptureInfo(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
-    method public int component1();
-    method public String component2();
-    method public String component3();
-    method public androidx.benchmark.macro.StartupMode? component4();
-    method public androidx.benchmark.macro.Metric.CaptureInfo copy(int apiLevel, String targetPackageName, String testPackageName, androidx.benchmark.macro.StartupMode? startupMode);
-    method public int getApiLevel();
-    method public androidx.benchmark.macro.StartupMode? getStartupMode();
-    method public String getTargetPackageName();
-    method public String getTestPackageName();
-    property public final int apiLevel;
-    property public final androidx.benchmark.macro.StartupMode? startupMode;
-    property public final String targetPackageName;
-    property public final String testPackageName;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static final class Metric.Measurement {
-    ctor public Metric.Measurement(String name, double data);
-    ctor public Metric.Measurement(String name, java.util.List<java.lang.Double> dataSamples);
-    method public String component1();
-    method public java.util.List<java.lang.Double> component2();
-    method public boolean component3();
-    method public androidx.benchmark.macro.Metric.Measurement copy(String name, java.util.List<java.lang.Double> data, boolean requireSingleValue);
-    method public java.util.List<java.lang.Double> getData();
-    method public String getName();
-    method public boolean getRequireSingleValue();
-    property public final java.util.List<java.lang.Double> data;
-    property public final String name;
-    property public final boolean requireSingleValue;
-  }
-
-  public final class MetricResultExtensionsKt {
-    method @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public static void assertEqualMeasurements(java.util.List<androidx.benchmark.macro.Metric.Measurement> expected, java.util.List<androidx.benchmark.macro.Metric.Measurement> observed, double threshold);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategory {
-    enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategoryDisplayLevel {
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
-  }
-
-  @SuppressCompatibility @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class PowerMetric extends androidx.benchmark.macro.Metric {
-    ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
-    method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
-    method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public static boolean deviceBatteryHasMinimumCharge();
-    method public static boolean deviceSupportsHighPrecisionTracking();
-    field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
-  }
-
-  public static final class PowerMetric.Companion {
-    method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
-    method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public boolean deviceBatteryHasMinimumCharge();
-    method public boolean deviceSupportsHighPrecisionTracking();
-  }
-
-  public abstract static sealed class PowerMetric.Type {
-    method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
-    method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
-    property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
-  }
-
-  public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Battery();
-  }
-
-  public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
-  }
-
-  public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PowerRail {
-    method public boolean hasMetrics(optional boolean throwOnMissingMetrics);
-    field public static final androidx.benchmark.macro.PowerRail INSTANCE;
-  }
-
-  public enum StartupMode {
-    enum_constant public static final androidx.benchmark.macro.StartupMode COLD;
-    enum_constant public static final androidx.benchmark.macro.StartupMode HOT;
-    enum_constant public static final androidx.benchmark.macro.StartupMode WARM;
-  }
-
-  @RequiresApi(29) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class StartupTimingLegacyMetric extends androidx.benchmark.macro.Metric {
-    ctor public StartupTimingLegacyMetric();
-  }
-
-  public final class StartupTimingMetric extends androidx.benchmark.macro.Metric {
-    ctor public StartupTimingMetric();
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public abstract class TraceMetric extends androidx.benchmark.macro.Metric {
-    ctor public TraceMetric();
-    method public abstract java.util.List<androidx.benchmark.macro.Metric.Measurement> getMeasurements(androidx.benchmark.macro.Metric.CaptureInfo captureInfo, androidx.benchmark.perfetto.PerfettoTraceProcessor.Session traceSession);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.macro.ExperimentalMetricApi public final class TraceSectionMetric extends androidx.benchmark.macro.Metric {
-    ctor public TraceSectionMetric(String sectionName);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label);
-    ctor public TraceSectionMetric(String sectionName, optional androidx.benchmark.macro.TraceSectionMetric.Mode mode, optional String label, optional boolean targetPackageOnly);
-  }
-
-  public abstract static sealed class TraceSectionMetric.Mode {
-  }
-
-  public static final class TraceSectionMetric.Mode.Average extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Average INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Count extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Count INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.First extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.First INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Max extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Max INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Min extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Min INSTANCE;
-  }
-
-  public static final class TraceSectionMetric.Mode.Sum extends androidx.benchmark.macro.TraceSectionMetric.Mode {
-    field public static final androidx.benchmark.macro.TraceSectionMetric.Mode.Sum INSTANCE;
-  }
-
-}
-
-package androidx.benchmark.perfetto {
-
-  @SuppressCompatibility @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoTraceProcessorApi {
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class PerfettoTraceProcessor {
-    ctor public PerfettoTraceProcessor();
-    method public <T> T loadTrace(androidx.benchmark.perfetto.PerfettoTrace trace, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor.Session,? extends T> block);
-    method public static <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    field public static final androidx.benchmark.perfetto.PerfettoTraceProcessor.Companion Companion;
-  }
-
-  public static final class PerfettoTraceProcessor.Companion {
-    method public <T> T runServer(kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-    method public static <T> T runServer(long timeout, kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTraceProcessor,? extends T> block);
-  }
-
-  public static final class PerfettoTraceProcessor.Session {
-    method public kotlin.sequences.Sequence<androidx.benchmark.perfetto.Row> query(@org.intellij.lang.annotations.Language("sql") String query);
-    method public String queryMetricsJson(java.util.List<java.lang.String> metrics);
-    method public byte[] queryMetricsProtoBinary(java.util.List<java.lang.String> metrics);
-    method public String queryMetricsProtoText(java.util.List<java.lang.String> metrics);
-    method public byte[] rawQuery(@org.intellij.lang.annotations.Language("sql") String query);
-  }
-
-  @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public final class Row implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<java.lang.String,java.lang.Object?> {
-    ctor public Row(java.util.Map<java.lang.String,? extends java.lang.Object?> map);
-    method public byte[] bytes(String columnName);
-    method public double double(String columnName);
-    method public long long(String columnName);
-    method public byte[]? nullableBytes(String columnName);
-    method public Double? nullableDouble(String columnName);
-    method public Long? nullableLong(String columnName);
-    method public String? nullableString(String columnName);
-    method public String string(String columnName);
-  }
-
-  public final class RowKt {
-    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi public static androidx.benchmark.perfetto.Row rowOf(kotlin.Pair<java.lang.String,? extends java.lang.Object?>... pairs);
-  }
-
-}
-
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index fbe028a..f038350 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -28,6 +28,7 @@
             proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
         }
     }
+    compileSdkVersion 35
     namespace "androidx.benchmark.integration.macrobenchmark.target"
 }
 
diff --git a/biometric/biometric-ktx/build.gradle b/biometric/biometric-ktx/build.gradle
index 0a615c2..740992b 100644
--- a/biometric/biometric-ktx/build.gradle
+++ b/biometric/biometric-ktx/build.gradle
@@ -47,5 +47,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.biometric.ktx"
 }
diff --git a/biometric/biometric-ktx/samples/build.gradle b/biometric/biometric-ktx/samples/build.gradle
index 0e63ec0..c750a94 100644
--- a/biometric/biometric-ktx/samples/build.gradle
+++ b/biometric/biometric-ktx/samples/build.gradle
@@ -42,5 +42,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.biometric.samples"
 }
diff --git a/biometric/biometric/api/current.txt b/biometric/biometric/api/current.txt
index 6a60ed1..eb18aba 100644
--- a/biometric/biometric/api/current.txt
+++ b/biometric/biometric/api/current.txt
@@ -43,6 +43,7 @@
     field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
     field public static final int ERROR_LOCKOUT = 7; // 0x7
     field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
     field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
     field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
     field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
@@ -72,16 +73,22 @@
     ctor public BiometricPrompt.CryptoObject(java.security.Signature);
     ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
     ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
     method public javax.crypto.Cipher? getCipher();
     method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
     method public javax.crypto.Mac? getMac();
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
     method public java.security.Signature? getSignature();
   }
 
   public static class BiometricPrompt.PromptInfo {
     method public int getAllowedAuthenticators();
+    method public androidx.biometric.PromptContentView? getContentView();
     method public CharSequence? getDescription();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
+    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
     method public CharSequence getNegativeButtonText();
     method public CharSequence? getSubtitle();
     method public CharSequence getTitle();
@@ -94,13 +101,54 @@
     method public androidx.biometric.BiometricPrompt.PromptInfo build();
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
     method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
   }
 
+  public interface PromptContentItem {
+  }
+
+  public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
+    ctor public PromptContentItemBulletedText(String);
+  }
+
+  public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
+    ctor public PromptContentItemPlainText(String);
+  }
+
+  public interface PromptContentView {
+  }
+
+  public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
+  }
+
+  public static final class PromptContentViewWithMoreOptionsButton.Builder {
+    ctor public PromptContentViewWithMoreOptionsButton.Builder();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
+  }
+
+  public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
+    method public String? getDescription();
+    method public java.util.List<androidx.biometric.PromptContentItem!> getListItems();
+  }
+
+  public static final class PromptVerticalListContentView.Builder {
+    ctor public PromptVerticalListContentView.Builder();
+    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
+    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
+    method public androidx.biometric.PromptVerticalListContentView build();
+    method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
+  }
+
 }
 
 package androidx.biometric.auth {
diff --git a/biometric/biometric/api/restricted_current.txt b/biometric/biometric/api/restricted_current.txt
index 6a60ed1..eb18aba 100644
--- a/biometric/biometric/api/restricted_current.txt
+++ b/biometric/biometric/api/restricted_current.txt
@@ -43,6 +43,7 @@
     field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
     field public static final int ERROR_LOCKOUT = 7; // 0x7
     field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
     field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
     field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
     field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
@@ -72,16 +73,22 @@
     ctor public BiometricPrompt.CryptoObject(java.security.Signature);
     ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
     ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
     method public javax.crypto.Cipher? getCipher();
     method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
     method public javax.crypto.Mac? getMac();
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
     method public java.security.Signature? getSignature();
   }
 
   public static class BiometricPrompt.PromptInfo {
     method public int getAllowedAuthenticators();
+    method public androidx.biometric.PromptContentView? getContentView();
     method public CharSequence? getDescription();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
+    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
     method public CharSequence getNegativeButtonText();
     method public CharSequence? getSubtitle();
     method public CharSequence getTitle();
@@ -94,13 +101,54 @@
     method public androidx.biometric.BiometricPrompt.PromptInfo build();
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
+    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
     method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
     method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
   }
 
+  public interface PromptContentItem {
+  }
+
+  public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
+    ctor public PromptContentItemBulletedText(String);
+  }
+
+  public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
+    ctor public PromptContentItemPlainText(String);
+  }
+
+  public interface PromptContentView {
+  }
+
+  public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
+  }
+
+  public static final class PromptContentViewWithMoreOptionsButton.Builder {
+    ctor public PromptContentViewWithMoreOptionsButton.Builder();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
+    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
+  }
+
+  public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
+    method public String? getDescription();
+    method public java.util.List<androidx.biometric.PromptContentItem!> getListItems();
+  }
+
+  public static final class PromptVerticalListContentView.Builder {
+    ctor public PromptVerticalListContentView.Builder();
+    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
+    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
+    method public androidx.biometric.PromptVerticalListContentView build();
+    method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
+  }
+
 }
 
 package androidx.biometric.auth {
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index c7b5373..61506c5 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -28,6 +28,10 @@
     id("com.android.library")
 }
 
+android {
+    compileSdk = 35
+}
+
 dependencies {
     // Public API dependencies
     api("androidx.annotation:annotation:1.6.0")
@@ -69,6 +73,7 @@
     }
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.biometric"
+    compileSdk = 35
 }
 
 androidx {
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
index c29b0bb..36b6817 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
@@ -16,11 +16,13 @@
 
 package androidx.biometric;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -81,13 +83,20 @@
     static final int CANCELED_FROM_CLIENT = 3;
 
     /**
+     * Authentication was canceled by the user by pressing the more options button on the prompt
+     * content.
+     */
+    static final int CANCELED_FROM_MORE_OPTIONS_BUTTON = 4;
+
+    /**
      * Where authentication was canceled from.
      */
     @IntDef({
         CANCELED_FROM_INTERNAL,
         CANCELED_FROM_USER,
         CANCELED_FROM_NEGATIVE_BUTTON,
-        CANCELED_FROM_CLIENT
+        CANCELED_FROM_CLIENT,
+        CANCELED_FROM_MORE_OPTIONS_BUTTON
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface CanceledFrom {}
@@ -352,6 +361,15 @@
                     }
                 });
 
+        mViewModel.isMoreOptionsButtonPressPending().observe(this,
+                moreOptionsButtonPressPending -> {
+                    if (moreOptionsButtonPressPending) {
+                        onMoreOptionsButtonPressed();
+                        mViewModel.setMoreOptionsButtonPressPending(false);
+                    }
+                }
+        );
+
         mViewModel.isFingerprintDialogCancelPending().observe(this,
                 fingerprintDialogCancelPending -> {
                     if (fingerprintDialogCancelPending) {
@@ -514,6 +532,29 @@
                     builder, AuthenticatorUtils.isDeviceCredentialAllowed(authenticators));
         }
 
+        // Set the custom biometric prompt features introduced in Android 15 (API 35).
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            final int logoRes = mViewModel.getLogoRes();
+            final Bitmap logoBitmap = mViewModel.getLogoBitmap();
+            final String logoDescription = mViewModel.getLogoDescription();
+            final android.hardware.biometrics.PromptContentView contentView =
+                    PromptContentViewUtils.wrapForBiometricPrompt(mViewModel.getContentView(),
+                            mViewModel.getClientExecutor(),
+                            mViewModel.getMoreOptionsButtonListener());
+            if (logoRes != -1) {
+                Api35Impl.setLogoRes(builder, logoRes);
+            }
+            if (logoBitmap != null) {
+                Api35Impl.setLogoBitmap(builder, logoBitmap);
+            }
+            if (logoDescription != null && !logoDescription.isEmpty()) {
+                Api35Impl.setLogoDescription(builder, logoDescription);
+            }
+            if (contentView != null) {
+                Api35Impl.setContentView(builder, contentView);
+            }
+        }
+
         authenticateWithBiometricPrompt(Api28Impl.buildPrompt(builder), getContext());
     }
 
@@ -779,6 +820,16 @@
     }
 
     /**
+     * Callback that is run when the view model reports that the more options button has been
+     * pressed on the prompt content.
+     */
+    void onMoreOptionsButtonPressed() {
+        sendErrorAndDismiss(BiometricPrompt.ERROR_MORE_OPTIONS_BUTTON,
+                "More options button in the content view is clicked.");
+        cancelAuthentication(BiometricFragment.CANCELED_FROM_MORE_OPTIONS_BUTTON);
+    }
+
+    /**
      * Launches the confirm device credential Settings activity, where the user can authenticate
      * using their PIN, pattern, or password.
      */
@@ -1097,6 +1148,72 @@
     }
 
     /**
+     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @SuppressLint("MissingPermission")
+    private static class Api35Impl {
+        // Prevent instantiation.
+        private Api35Impl() {
+        }
+
+        /**
+         * Sets the prompt content view for the given framework prompt builder.
+         *
+         * @param builder An instance of
+         *                {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+         * @param logoRes A drawable resource of the logo that will be shown on the prompt.
+         */
+        @DoNotInline
+        static void setLogoRes(
+                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder, int logoRes) {
+            builder.setLogoRes(logoRes);
+        }
+
+        /**
+         * Sets the prompt content view for the given framework prompt builder.
+         *
+         * @param builder    An instance of
+         *                   {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+         * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
+         */
+        @DoNotInline
+        static void setLogoBitmap(
+                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+                @NonNull Bitmap logoBitmap) {
+            builder.setLogoBitmap(logoBitmap);
+        }
+
+        /**
+         * Sets the prompt content view for the given framework prompt builder.
+         *
+         * @param builder         An instance of
+         *                        {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+         * @param logoDescription The content view for the prompt.
+         */
+        @DoNotInline
+        static void setLogoDescription(
+                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+                String logoDescription) {
+            builder.setLogoDescription(logoDescription);
+        }
+
+        /**
+         * Sets the prompt content view for the given framework prompt builder.
+         *
+         * @param builder     An instance of
+         *                    {@link android.hardware.biometrics.BiometricPrompt.Builder}.
+         * @param contentView The content view for the prompt.
+         */
+        @DoNotInline
+        static void setContentView(
+                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
+                @NonNull android.hardware.biometrics.PromptContentView contentView) {
+            builder.setContentView(contentView);
+        }
+    }
+
+    /**
      * Nested class to avoid verification errors for methods introduced in Android 11 (API 30).
      */
     @RequiresApi(Build.VERSION_CODES.R)
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
index ed361eb..01ff4b7 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
@@ -16,15 +16,20 @@
 
 package androidx.biometric;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
+
 import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.DrawableRes;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.biometric.BiometricManager.Authenticators;
@@ -160,6 +165,11 @@
     public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15;
 
     /**
+     * The user pressed the more options button on prompt content.
+     */
+    public static final int ERROR_MORE_OPTIONS_BUTTON = 16;
+
+    /**
      * An error code that may be returned during authentication.
      */
     @IntDef({
@@ -175,7 +185,8 @@
         ERROR_NO_BIOMETRICS,
         ERROR_HW_NOT_PRESENT,
         ERROR_NEGATIVE_BUTTON,
-        ERROR_NO_DEVICE_CREDENTIAL
+        ERROR_NO_DEVICE_CREDENTIAL,
+        ERROR_MORE_OPTIONS_BUTTON
     })
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
@@ -230,6 +241,7 @@
         @Nullable private final Mac mMac;
         @Nullable private final android.security.identity.IdentityCredential mIdentityCredential;
         @Nullable private final android.security.identity.PresentationSession mPresentationSession;
+        private final long mOperationHandle;
 
         /**
          * Creates a crypto object that wraps the given signature object.
@@ -242,6 +254,7 @@
             mMac = null;
             mIdentityCredential = null;
             mPresentationSession = null;
+            mOperationHandle = 0;
         }
 
         /**
@@ -255,6 +268,7 @@
             mMac = null;
             mIdentityCredential = null;
             mPresentationSession = null;
+            mOperationHandle = 0;
         }
 
         /**
@@ -268,6 +282,7 @@
             mMac = mac;
             mIdentityCredential = null;
             mPresentationSession = null;
+            mOperationHandle = 0;
         }
 
         /**
@@ -284,6 +299,7 @@
             mMac = null;
             mIdentityCredential = identityCredential;
             mPresentationSession = null;
+            mOperationHandle = 0;
         }
 
         /**
@@ -300,9 +316,27 @@
             mMac = null;
             mIdentityCredential = null;
             mPresentationSession = presentationSession;
+            mOperationHandle = 0;
         }
 
         /**
+         * Create from an operation handle.
+         * @see CryptoObject#getOperationHandle()
+         *
+         * @param operationHandle the operation handle associated with this object.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        public CryptoObject(long operationHandle) {
+            mSignature = null;
+            mCipher = null;
+            mMac = null;
+            mIdentityCredential = null;
+            mPresentationSession = null;
+            mOperationHandle = operationHandle;
+        }
+
+
+        /**
          * Gets the signature object associated with this crypto object.
          *
          * @return The signature, or {@code null} if none is associated with this object.
@@ -353,6 +387,34 @@
         public android.security.identity.PresentationSession getPresentationSession() {
             return mPresentationSession;
         }
+
+        /**
+         * Returns the {@code operationHandle} associated with this object or 0 if none.
+         * The {@code operationHandle} is the underlying identifier associated with
+         * the {@code CryptoObject}.
+         *
+         * <p> The {@code operationHandle} can be used to reconstruct a {@code CryptoObject}
+         * instance. This is useful for any cross-process communication as the {@code CryptoObject}
+         * class is not {@link android.os.Parcelable}. Hence, if the {@code CryptoObject} is
+         * constructed in one process, and needs to be propagated to another process,
+         * before calling the {@code authenticate()} API in the second process, the
+         * recommendation is to retrieve the {@code operationHandle} using this API, and then
+         * reconstruct the {@code CryptoObject}using the constructor that takes in an {@code
+         * operationHandle}, and pass that in to the {@code authenticate} API mentioned above.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        public long getOperationHandle() {
+            return CryptoObjectUtils.getOperationHandle(this);
+        }
+
+        /**
+         * Returns the {@code operationHandle} from the constructor. This is only for wrapping
+         * this {@link androidx.biometric.BiometricPrompt.CryptoObject} to
+         * {@link android.hardware.biometrics.BiometricPrompt}.
+         */
+        long getOperationHandleCryptoObject() {
+            return mOperationHandle;
+        }
     }
 
     /**
@@ -438,15 +500,75 @@
          */
         public static class Builder {
             // Mutable options to be set on the builder.
+            @DrawableRes private int mLogoRes = -1;
+            @Nullable private Bitmap mLogoBitmap = null;
+            @Nullable private String mLogoDescription = null;
             @Nullable private CharSequence mTitle = null;
             @Nullable private CharSequence mSubtitle = null;
             @Nullable private CharSequence mDescription = null;
+            @Nullable private PromptContentView mPromptContentView = null;
             @Nullable private CharSequence mNegativeButtonText = null;
             private boolean mIsConfirmationRequired = true;
             private boolean mIsDeviceCredentialAllowed = false;
             @BiometricManager.AuthenticatorTypes private int mAllowedAuthenticators = 0;
 
             /**
+             * Optional: Sets the drawable resource of the logo that will be shown on the prompt.
+             *
+             * <p> Note that using this method is not recommended in most scenarios because the
+             * calling application's icon will be used by default. Setting the logo is intended
+             * for large bundled applications that perform a wide range of functions and need to
+             * show distinct icons for each function.
+             *
+             * @param logoRes A drawable resource of the logo that will be shown on the prompt.
+             * @return This builder.
+             */
+            @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+            @NonNull
+            public Builder setLogoRes(@DrawableRes int logoRes) {
+                mLogoRes = logoRes;
+                return this;
+            }
+
+            /**
+             * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt.
+             *
+             * <p> Note that using this method is not recommended in most scenarios because the
+             * calling application's icon will be used by default. Setting the logo is intended
+             * for large bundled applications that perform a wide range of functions and need to
+             * show distinct icons for each function.
+             *
+             * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
+             * @return This builder.
+             */
+            @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+            @NonNull
+            public Builder setLogoBitmap(@NonNull Bitmap logoBitmap) {
+                mLogoBitmap = logoBitmap;
+                return this;
+            }
+
+            /**
+             * Optional: Sets logo description text that will be shown on the prompt.
+             *
+             * <p> Note that using this method is not recommended in most scenarios because the
+             * calling application's name will be used by default. Setting the logo description
+             * is intended for large bundled applications that perform a wide range of functions
+             * and need to show distinct description for each function.
+             *
+             * @param logoDescription The logo description text that will be shown on the prompt.
+             * @return This builder.
+             * @throws IllegalArgumentException If logo description is null or exceeds certain
+             *                                  character limit.
+             */
+            @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+            @NonNull
+            public Builder setLogoDescription(@NonNull String logoDescription) {
+                mLogoDescription = logoDescription;
+                return this;
+            }
+
+            /**
              * Required: Sets the title for the prompt.
              *
              * @param title The title to be displayed on the prompt.
@@ -471,9 +593,14 @@
             }
 
             /**
-             * Optional: Sets the description for the prompt.
+             * Optional: Sets a description that will be shown on the prompt.
              *
-             * @param description The description to be displayed on the prompt.
+             * <p> Note that the description set by {@link Builder#setDescription(CharSequence)}
+             * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view
+             * provided to {@link Builder#setContentView(PromptContentView)} will be used if both
+             * methods are called.
+             *
+             * @param description The description to display.
              * @return This builder.
              */
             @NonNull
@@ -483,6 +610,23 @@
             }
 
             /**
+             * Optional: Sets application customized content view that will be shown on the prompt.
+             *
+             * <p> Note that the description set by {@link Builder#setDescription(CharSequence)}
+             * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view
+             * provided to {@link Builder#setContentView(PromptContentView)} will be used if both
+             * methods are called.
+             *
+             * @param view The customized view information.
+             * @return This builder.
+             */
+            @NonNull
+            public Builder setContentView(@NonNull PromptContentView view) {
+                mPromptContentView = view;
+                return this;
+            }
+
+            /**
              * Required: Sets the text for the negative button on the prompt.
              *
              * <p>Note that this option is incompatible with device credential authentication and
@@ -623,9 +767,13 @@
                 }
 
                 return new PromptInfo(
+                        mLogoRes,
+                        mLogoBitmap,
+                        mLogoDescription,
                         mTitle,
                         mSubtitle,
                         mDescription,
+                        mPromptContentView,
                         mNegativeButtonText,
                         mIsConfirmationRequired,
                         mIsDeviceCredentialAllowed,
@@ -634,9 +782,13 @@
         }
 
         // Immutable fields for the prompt info object.
+        @DrawableRes private int mLogoRes;
+        @Nullable private Bitmap mLogoBitmap;
+        @Nullable private String mLogoDescription;
         @NonNull private final CharSequence mTitle;
         @Nullable private final CharSequence mSubtitle;
         @Nullable private final CharSequence mDescription;
+        @Nullable private PromptContentView mPromptContentView;
         @Nullable private final CharSequence mNegativeButtonText;
         private final boolean mIsConfirmationRequired;
         private final boolean mIsDeviceCredentialAllowed;
@@ -645,16 +797,24 @@
         // Prevent direct instantiation.
         @SuppressWarnings("WeakerAccess") /* synthetic access */
         PromptInfo(
+                int logoRes,
+                @Nullable Bitmap logoBitmap,
+                @Nullable String logoDescription,
                 @NonNull CharSequence title,
                 @Nullable CharSequence subtitle,
                 @Nullable CharSequence description,
+                @Nullable PromptContentView promptContentView,
                 @Nullable CharSequence negativeButtonText,
                 boolean confirmationRequired,
                 boolean deviceCredentialAllowed,
                 @BiometricManager.AuthenticatorTypes int allowedAuthenticators) {
+            mLogoRes = logoRes;
+            mLogoBitmap = logoBitmap;
+            mLogoDescription = logoDescription;
             mTitle = title;
             mSubtitle = subtitle;
             mDescription = description;
+            mPromptContentView = promptContentView;
             mNegativeButtonText = negativeButtonText;
             mIsConfirmationRequired = confirmationRequired;
             mIsDeviceCredentialAllowed = deviceCredentialAllowed;
@@ -662,6 +822,44 @@
         }
 
         /**
+         * Gets the drawable resource of the logo for the prompt, as set by
+         * {@link Builder#setLogoRes(int)}. Currently for system applications use only.
+         *
+         * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set.
+         */
+        @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+        @DrawableRes
+        public int getLogoRes() {
+            return mLogoRes;
+        }
+
+        /**
+         * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}.
+         * Currently for system applications use only.
+         *
+         * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set.
+         */
+        @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+        @Nullable
+        public Bitmap getLogoBitmap() {
+            return mLogoBitmap;
+        }
+
+        /**
+         * Gets the logo description for the prompt, as set by
+         * {@link Builder#setLogoDescription(String)}.
+         * Currently for system applications use only.
+         *
+         * @return The logo description of the prompt, or null if the prompt has no logo description
+         * set.
+         */
+        @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+        @Nullable
+        public String getLogoDescription() {
+            return mLogoDescription;
+        }
+
+        /**
          * Gets the title for the prompt.
          *
          * @return The title to be displayed on the prompt.
@@ -698,6 +896,17 @@
         }
 
         /**
+         * Gets the content view for the prompt, as set by
+         * {@link Builder#setContentView(PromptContentView)}.
+         *
+         * @return The content view for the prompt, or null if the prompt has no content view.
+         */
+        @Nullable
+        public PromptContentView getContentView() {
+            return mPromptContentView;
+        }
+
+        /**
          * Gets the text for the negative button on the prompt.
          *
          * @return The label to be used for the negative button on the prompt, or an empty string if
diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
index 56f8782..eb6970f 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
@@ -16,7 +16,9 @@
 
 package androidx.biometric;
 
+import android.annotation.SuppressLint;
 import android.content.DialogInterface;
+import android.graphics.Bitmap;
 import android.os.Handler;
 import android.os.Looper;
 
@@ -139,6 +141,29 @@
     }
 
     /**
+     * The dialog listener that is returned by {@link #getMoreOptionsButtonListener()} ()}.
+     */
+    private static class MoreOptionsButtonListener implements DialogInterface.OnClickListener {
+        @NonNull private final WeakReference<BiometricViewModel> mViewModelRef;
+
+        /**
+         * Creates a more options button listener with a weak reference to the given view model.
+         *
+         * @param viewModel The view model instance to hold a weak reference to.
+         */
+        MoreOptionsButtonListener(@Nullable BiometricViewModel viewModel) {
+            mViewModelRef = new WeakReference<>(viewModel);
+        }
+
+        @Override
+        public void onClick(DialogInterface dialogInterface, int which) {
+            if (mViewModelRef.get() != null) {
+                mViewModelRef.get().setMoreOptionsButtonPressPending(true);
+            }
+        }
+    }
+
+    /**
      * The executor that will run authentication callback methods.
      *
      * <p>If unset, callbacks are invoked on the main thread with {@link Looper#getMainLooper()}.
@@ -181,6 +206,11 @@
     @Nullable private DialogInterface.OnClickListener mNegativeButtonListener;
 
     /**
+     * A dialog listener for the more options button shown on the prompt content.
+     */
+    @Nullable private DialogInterface.OnClickListener mMoreOptionsButtonListener;
+
+    /**
      * A label for the negative button shown on the prompt.
      *
      * <p>If set, this value overrides the one returned by
@@ -251,6 +281,11 @@
     @Nullable private MutableLiveData<Boolean> mIsNegativeButtonPressPending;
 
     /**
+     * Whether the user has pressed the more options button on the prompt content.
+     */
+    @Nullable private MutableLiveData<Boolean> mIsMoreOptionsButtonPressPending;
+
+    /**
      * Whether the fingerprint dialog should always be dismissed instantly.
      */
     private boolean mIsFingerprintDialogDismissedInstantly = true;
@@ -327,6 +362,47 @@
     }
 
     /**
+     * Gets the logo res to be shown on the biometric prompt.
+     *
+     * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+     * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+     *
+     * @return The logo res for the prompt, or -1 if not set.
+     */
+    @SuppressLint("MissingPermission")
+    int getLogoRes() {
+        return mPromptInfo != null ? mPromptInfo.getLogoRes() : -1;
+    }
+
+    /**
+     * Gets the logo bitmap to be shown on the biometric prompt.
+     *
+     * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+     * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+     *
+     * @return The logo bitmap for the prompt, or null if not set.
+     */
+    @SuppressLint("MissingPermission")
+    @Nullable
+    Bitmap getLogoBitmap() {
+        return mPromptInfo != null ? mPromptInfo.getLogoBitmap() : null;
+    }
+
+    /**
+     * Gets the logo description to be shown on the biometric prompt.
+     *
+     * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+     * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+     *
+     * @return The logo description for the prompt, or null if not set.
+     */
+    @SuppressLint("MissingPermission")
+    @Nullable
+    String getLogoDescription() {
+        return mPromptInfo != null ? mPromptInfo.getLogoDescription() : null;
+    }
+
+    /**
      * Gets the title to be shown on the biometric prompt.
      *
      * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
@@ -366,6 +442,19 @@
     }
 
     /**
+     * Gets the prompt content view to be shown on the biometric prompt.
+     *
+     * <p>This method relies on the {@link BiometricPrompt.PromptInfo} set by
+     * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}.
+     *
+     * @return The prompt content view for the prompt, or {@code null} if not set.
+     */
+    @Nullable
+    PromptContentView getContentView() {
+        return mPromptInfo != null ? mPromptInfo.getContentView() : null;
+    }
+
+    /**
      * Gets the text that should be shown for the negative button on the biometric prompt.
      *
      * <p>If non-null, the value set by {@link #setNegativeButtonTextOverride(CharSequence)} is
@@ -454,6 +543,14 @@
         return mNegativeButtonListener;
     }
 
+    @NonNull
+    DialogInterface.OnClickListener getMoreOptionsButtonListener() {
+        if (mMoreOptionsButtonListener == null) {
+            mMoreOptionsButtonListener = new MoreOptionsButtonListener(this);
+        }
+        return mMoreOptionsButtonListener;
+    }
+
     void setNegativeButtonTextOverride(@Nullable CharSequence negativeButtonTextOverride) {
         mNegativeButtonTextOverride = negativeButtonTextOverride;
     }
@@ -593,6 +690,22 @@
         updateValue(mIsNegativeButtonPressPending, negativeButtonPressPending);
     }
 
+    @NonNull
+    LiveData<Boolean> isMoreOptionsButtonPressPending() {
+        if (mIsMoreOptionsButtonPressPending == null) {
+            mIsMoreOptionsButtonPressPending = new MutableLiveData<>();
+        }
+        return mIsMoreOptionsButtonPressPending;
+    }
+
+    void setMoreOptionsButtonPressPending(boolean moreOptionsButtonPressPending) {
+        if (mIsMoreOptionsButtonPressPending == null) {
+            mIsMoreOptionsButtonPressPending = new MutableLiveData<>();
+        }
+        updateValue(mIsMoreOptionsButtonPressPending, moreOptionsButtonPressPending);
+    }
+
+
     boolean isFingerprintDialogDismissedInstantly() {
         return mIsFingerprintDialogDismissedInstantly;
     }
diff --git a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
index 4cc0c30..871135d 100644
--- a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
+++ b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
@@ -112,6 +112,17 @@
             }
         }
 
+        // Operation handle is only supported on API 35 and above.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            // This should be the bottom one and only be reachable when cryptoObject was
+            // constructed with operation handle. cryptoObject from other constructors should
+            // already be unwrapped and returned above.
+            final long operationHandle = Api35Impl.getOperationHandle(cryptoObject);
+            if (operationHandle != 0) {
+                return new BiometricPrompt.CryptoObject(operationHandle);
+            }
+        }
+
         return null;
     }
 
@@ -164,10 +175,39 @@
             }
         }
 
+        // Operation handle is only supported on API 35 and above.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            final long operationHandle = cryptoObject.getOperationHandleCryptoObject();
+            if (operationHandle != 0) {
+                return Api35Impl.create(operationHandle);
+            }
+        }
+
         return null;
     }
 
     /**
+     * Get the {@code operationHandle} associated with this object or 0 if none. This needs to be
+     * achieved by getting the corresponding
+     * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} and then get its
+     * operation handle.
+     *
+     * @param cryptoObject An instance of {@link androidx.biometric.BiometricPrompt.CryptoObject}.
+     * @return The {@code operationHandle} associated with this object or 0 if none.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    static long getOperationHandle(@Nullable BiometricPrompt.CryptoObject cryptoObject) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCryptoObject =
+                    CryptoObjectUtils.wrapForBiometricPrompt(cryptoObject);
+            if (wrappedCryptoObject != null) {
+                return Api35Impl.getOperationHandle(wrappedCryptoObject);
+            }
+        }
+        return 0;
+    }
+
+    /**
      * Unwraps a crypto object returned by
      * {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}.
      *
@@ -250,6 +290,11 @@
             return null;
         }
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            Log.e(TAG, "Operation handle is not supported by FingerprintManager.");
+            return null;
+        }
+
         return null;
     }
 
@@ -298,6 +343,41 @@
     }
 
     /**
+     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private static class Api35Impl {
+        // Prevent instantiation.
+        private Api35Impl() {}
+
+        /**
+         * Creates an instance of the framework class
+         * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} from the given
+         * operation handle.
+         *
+         * @param operationHandle The operation handle to be wrapped.
+         * @return An instance of {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
+         */
+        @NonNull
+        static android.hardware.biometrics.BiometricPrompt.CryptoObject create(
+                long operationHandle) {
+            return new android.hardware.biometrics.BiometricPrompt.CryptoObject(operationHandle);
+        }
+
+        /**
+         * Gets the operation handle associated with the given crypto object, if any.
+         *
+         * @param crypto An instance of
+         *               {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
+         * @return The wrapped operation handle object, or {@code null}.
+         */
+        static long getOperationHandle(
+                @NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject crypto) {
+            return crypto.getOperationHandle();
+        }
+    }
+
+    /**
      * Nested class to avoid verification errors for methods introduced in Android 13.0 (API 33).
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
new file mode 100644
index 0000000..099094a
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+/**
+ * An item shown on {@link PromptContentView}.
+ */
+public interface PromptContentItem {
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java
new file mode 100644
index 0000000..6ec55e2
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ */
+public final class PromptContentItemBulletedText implements PromptContentItem {
+    private final String mText;
+
+    /**
+     * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentItemBulletedText(@NonNull String text) {
+        mText = text;
+    }
+
+    @NonNull
+    String getText() {
+        return mText;
+    }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java
new file mode 100644
index 0000000..c7f2b6f
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ */
+public final class PromptContentItemPlainText implements PromptContentItem {
+    private final String mText;
+
+    /**
+     * A list item with plain text shown on {@link PromptVerticalListContentView}.
+     *
+     * @param text The text of this list item.
+     */
+    public PromptContentItemPlainText(@NonNull String text) {
+        mText = text;
+    }
+
+    @NonNull
+    String getText() {
+        return mText;
+    }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
new file mode 100644
index 0000000..090960b
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+/**
+ * Contains the information of the template of content view for Biometric Prompt.
+ */
+public interface PromptContentView {
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
new file mode 100644
index 0000000..cb6679a8
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+import android.annotation.SuppressLint;
+import android.content.DialogInterface;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Utility class for creating and converting between different types of prompt content view that may
+ * be used internally by {@link BiometricPrompt}
+ */
+class PromptContentViewUtils {
+    // Prevent instantiation.
+    private PromptContentViewUtils() {
+    }
+
+    /**
+     * Wraps a prompt content view to be passed to {@link BiometricPrompt}.
+     *
+     * @param contentView               An instance of {@link PromptContentView}.
+     * @param executor                  An executor for the more options button callback.
+     * @param moreOptionsButtonListener A listener for the more options button press event.
+     * @return An equivalent prompt content view that is compatible with
+     * {@link android.hardware.biometrics.PromptContentView}.
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Nullable
+    static android.hardware.biometrics.PromptContentView wrapForBiometricPrompt(
+            @Nullable PromptContentView contentView, @NonNull Executor executor,
+            @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
+
+        if (contentView == null) {
+            return null;
+        }
+
+        // Prompt content view is only supported on API 35 and above.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            if (contentView instanceof PromptVerticalListContentView) {
+                return Api35Impl.createPromptVerticalListContentView(
+                        (PromptVerticalListContentView) contentView);
+            } else if (contentView instanceof PromptContentViewWithMoreOptionsButton) {
+                return Api35Impl.createPromptContentViewWithMoreOptionsButton(
+                        (PromptContentViewWithMoreOptionsButton) contentView, executor,
+                        moreOptionsButtonListener);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @SuppressLint("MissingPermission")
+    private static class Api35Impl {
+        // Prevent instantiation.
+        private Api35Impl() {
+        }
+
+        /**
+         * Creates an instance of the framework class
+         * {@link android.hardware.biometrics.PromptVerticalListContentView} from the given
+         * content view.
+         *
+         * @param contentView The prompt content view to be wrapped.
+         * @return An instance of {@link android.hardware.biometrics.PromptVerticalListContentView}.
+         */
+        @NonNull
+        static android.hardware.biometrics.PromptContentView createPromptVerticalListContentView(
+                @NonNull PromptVerticalListContentView contentView) {
+            android.hardware.biometrics.PromptVerticalListContentView.Builder
+                    contentViewBuilder =
+                    new android.hardware.biometrics.PromptVerticalListContentView.Builder();
+            if (contentView.getDescription() != null) {
+                contentViewBuilder.setDescription(contentView.getDescription());
+            }
+            contentView.getListItems().forEach(
+                    it -> {
+                        if (it instanceof PromptContentItemPlainText) {
+                            contentViewBuilder.addListItem(
+                                    new android.hardware.biometrics.PromptContentItemPlainText(
+                                            ((PromptContentItemPlainText) it).getText()));
+                        } else if (it instanceof PromptContentItemBulletedText) {
+                            contentViewBuilder.addListItem(
+                                    new android.hardware.biometrics.PromptContentItemBulletedText(
+                                            ((PromptContentItemBulletedText) it).getText()));
+                        }
+                    });
+            return contentViewBuilder.build();
+        }
+
+        /**
+         * Creates an instance of the framework class
+         * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton} from the
+         * given content view.
+         *
+         * @param contentView               The prompt content view to be wrapped.
+         * @param executor                  An executor for the more options button callback.
+         * @param moreOptionsButtonListener A listener for the more options button press event.
+         * @return An instance of
+         * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton}.
+         */
+        @NonNull
+        static android.hardware.biometrics.PromptContentView
+                createPromptContentViewWithMoreOptionsButton(
+                        @NonNull PromptContentViewWithMoreOptionsButton contentView,
+                        @NonNull Executor executor,
+                        @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
+            android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder
+                    contentViewBuilder =
+                    new android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
+                            .Builder();
+            if (contentView.getDescription() != null) {
+                contentViewBuilder.setDescription(contentView.getDescription());
+            }
+            contentViewBuilder.setMoreOptionsButtonListener(executor, moreOptionsButtonListener);
+            return contentViewBuilder.build();
+        }
+    }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
new file mode 100644
index 0000000..eb4f9aa
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+
+/**
+ * Contains the information of the template of content view with a more options button for
+ * Biometric Prompt.
+ * <p>
+ * This button should be used to provide more options for sign in or other purposes, such as when a
+ * user needs to select between multiple app-specific accounts or profiles that are available for
+ * sign in.
+ * <p>
+ * Apps should avoid using this when possible because it will create additional steps that the user
+ * must navigate through - clicking the more options button will dismiss the prompt, provide the app
+ * an opportunity to ask the user for the correct option, and finally allow the app to decide how to
+ * proceed once selected.
+ *
+ * <p>
+ * Here's how you'd set a <code>PromptContentViewWithMoreOptionsButton</code> on a Biometric
+ * Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
+ *     .setTitle(...)
+ *     .setSubTitle(...)
+ *     .setContentView(
+ *         new PromptContentViewWithMoreOptionsButton.Builder()
+ *             .setDescription("test description")
+ *             .setMoreOptionsButtonListener(executor, listener)
+ *             .build()
+ *      )
+ *     .build();
+ * </pre>
+ */
+public final class PromptContentViewWithMoreOptionsButton implements PromptContentView {
+    static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+
+    private final String mDescription;
+
+    private PromptContentViewWithMoreOptionsButton(@NonNull String description) {
+        mDescription = description;
+    }
+
+    /**
+     * Gets the description for the content view, as set by
+     * {@link PromptContentViewWithMoreOptionsButton.Builder#setDescription(String)}.
+     *
+     * @return The description for the content view, or null if the content view has no description.
+     */
+    @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+    @Nullable
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * A builder used to set individual options for the
+     * {@link PromptContentViewWithMoreOptionsButton} class.
+     */
+    public static final class Builder {
+        private String mDescription;
+
+        /**
+         * Optional: Sets a description that will be shown on the content view.
+         *
+         * @param description The description to display.
+         * @return This builder.
+         * @throws IllegalArgumentException If description exceeds certain character limit.
+         */
+        @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+        @NonNull
+        public Builder setDescription(@NonNull String description) {
+            if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
+                throw new IllegalArgumentException("The character number of description exceeds "
+                        + MAX_DESCRIPTION_CHARACTER_NUMBER);
+            }
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Creates a {@link PromptContentViewWithMoreOptionsButton}.
+         *
+         * @return An instance of {@link PromptContentViewWithMoreOptionsButton}.
+         */
+        @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
+        @NonNull
+        public PromptContentViewWithMoreOptionsButton build() {
+            return new PromptContentViewWithMoreOptionsButton(mDescription);
+        }
+    }
+}
diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
new file mode 100644
index 0000000..58d7be2
--- /dev/null
+++ b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.biometric;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains the information of the template of vertical list content view for Biometric Prompt.
+ * <p>
+ * Here's how you'd set a <code>PromptVerticalListContentView</code> on a Biometric Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
+ *     .setTitle(...)
+ *     .setSubTitle(...)
+ *     .setContentView(
+ *         new PromptVerticalListContentView.Builder()
+ *             .setDescription("test description")
+ *             .addListItem(new PromptContentItemPlainText("test item 1"))
+ *             .addListItem(new PromptContentItemPlainText("test item 2"))
+ *             .addListItem(new PromptContentItemBulletedText("test item 3"))
+ *             .build()
+ *      )
+ *     .build();
+ * </pre>
+ */
+public final class PromptVerticalListContentView implements PromptContentView {
+    static final int MAX_ITEM_NUMBER = 20;
+    static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+    static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+
+    private final List<PromptContentItem> mContentList;
+    private final String mDescription;
+
+    private PromptVerticalListContentView(@NonNull List<PromptContentItem> contentList,
+            @NonNull String description) {
+        mContentList = contentList;
+        mDescription = description;
+    }
+
+    /**
+     * Gets the description for the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#setDescription(String)}.
+     *
+     * @return The description for the content view, or null if the content view has no description.
+     */
+    @Nullable
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * Gets the list of items on the content view, as set by
+     * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}.
+     *
+     * @return The item list on the content view.
+     */
+    @NonNull
+    public List<PromptContentItem> getListItems() {
+        return new ArrayList<>(mContentList);
+    }
+
+    /**
+     * A builder used to set individual options for the {@link PromptVerticalListContentView} class.
+     */
+    public static final class Builder {
+        private final List<PromptContentItem> mContentList = new ArrayList<>();
+        private String mDescription;
+
+        /**
+         * Optional: Sets a description that will be shown on the content view.
+         *
+         * @param description The description to display.
+         * @return This builder.
+         * @throws IllegalArgumentException If description exceeds certain character limit.
+         */
+        @NonNull
+        public Builder setDescription(@NonNull String description) {
+            if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
+                throw new IllegalArgumentException("The character number of description exceeds "
+                        + MAX_DESCRIPTION_CHARACTER_NUMBER);
+            }
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Optional: Adds a list item in the current row.
+         *
+         * @param listItem The list item view to display
+         * @return This builder.
+         * @throws IllegalArgumentException If this list item exceeds certain character limits or
+         *                                  the number of list items exceeds certain limit.
+         */
+        @NonNull
+        public Builder addListItem(@NonNull PromptContentItem listItem) {
+            mContentList.add(listItem);
+            checkItemLimits(listItem);
+            return this;
+        }
+
+        /**
+         * Optional: Adds a list item in the current row.
+         *
+         * @param listItem The list item view to display
+         * @param index    The position at which to add the item
+         * @return This builder.
+         * @throws IllegalArgumentException If this list item exceeds certain character limits or
+         *                                  the number of list items exceeds certain limit.
+         */
+        @NonNull
+        public Builder addListItem(@NonNull PromptContentItem listItem, int index) {
+            mContentList.add(index, listItem);
+            checkItemLimits(listItem);
+            return this;
+        }
+
+        private void checkItemLimits(@NonNull PromptContentItem listItem) {
+            if (doesListItemExceedsCharLimit(listItem)) {
+                throw new IllegalArgumentException(
+                        "The character number of list item exceeds "
+                                + MAX_EACH_ITEM_CHARACTER_NUMBER);
+            }
+            if (mContentList.size() > MAX_ITEM_NUMBER) {
+                throw new IllegalArgumentException(
+                        "The number of list items exceeds " + MAX_ITEM_NUMBER);
+            }
+        }
+
+        private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) {
+            if (listItem instanceof PromptContentItemPlainText) {
+                return ((PromptContentItemPlainText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else if (listItem instanceof PromptContentItemBulletedText) {
+                return ((PromptContentItemBulletedText) listItem).getText().length()
+                        > MAX_EACH_ITEM_CHARACTER_NUMBER;
+            } else {
+                return false;
+            }
+        }
+
+
+        /**
+         * Creates a {@link PromptVerticalListContentView}.
+         *
+         * @return An instance of {@link PromptVerticalListContentView}.
+         */
+        @NonNull
+        public PromptVerticalListContentView build() {
+            return new PromptVerticalListContentView(mContentList, mDescription);
+        }
+    }
+}
+
diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
index 56a5342..a43ef4c 100644
--- a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
+++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.graphics.Bitmap;
 import android.os.Build;
 
 import androidx.biometric.BiometricManager.Authenticators;
@@ -58,6 +59,95 @@
         assertThat(allowedAuthenticators).isEqualTo(allowedAuthenticators);
     }
 
+    @Test
+    public void testPromptInfo_CanSetAndGetOptions_logoResAndDescription() {
+        final int logoRes = R.drawable.fingerprint_dialog_fp_icon;
+        final String logoDescription = "logo description";
+        final String title = "Title";
+        final String negativeButtonText = "Negative";
+
+        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+                .setLogoRes(logoRes)
+                .setLogoDescription(logoDescription)
+                .setLogoDescription(logoDescription)
+                .setTitle(title)
+                .setNegativeButtonText(negativeButtonText)
+                .build();
+
+        assertThat(info.getLogoRes()).isEqualTo(logoRes);
+        assertThat(info.getLogoDescription()).isEqualTo(logoDescription);
+    }
+
+    @Test
+    public void testPromptInfo_CanSetAndGetOptions_logoBitmap() {
+        final Bitmap logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565);
+        final String logoDescription = "logo description";
+        final String title = "Title";
+        final String negativeButtonText = "Negative";
+
+        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+                .setLogoBitmap(logoBitmap)
+                .setLogoDescription(logoDescription)
+                .setTitle(title)
+                .setNegativeButtonText(negativeButtonText)
+                .build();
+
+        assertThat(info.getLogoBitmap()).isEqualTo(logoBitmap);
+    }
+
+    @Test
+    public void testPromptInfo_CanSetAndGetOptions_verticalListContent() {
+        final String contentDescription = "test description";
+        final String itemOne = "content item 1";
+        final String itemTwo = "content item 2";
+        final PromptVerticalListContentView contentView =
+                new PromptVerticalListContentView.Builder()
+                        .setDescription(contentDescription)
+                        .addListItem(new PromptContentItemBulletedText(itemOne))
+                        .addListItem(new PromptContentItemBulletedText(itemTwo), 1).build();
+        final String title = "Title";
+        final String negativeButtonText = "Negative";
+
+        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+                .setTitle(title)
+                .setNegativeButtonText(negativeButtonText)
+                .setContentView(contentView)
+                .build();
+
+        assertThat(info.getContentView()).isEqualTo(contentView);
+        final PromptVerticalListContentView realContentView =
+                (PromptVerticalListContentView) info.getContentView();
+        assertThat(realContentView.getDescription()).isEqualTo(contentDescription);
+        final PromptContentItemBulletedText realItemOne =
+                (PromptContentItemBulletedText) realContentView.getListItems().get(0);
+        assertThat(realItemOne.getText()).isEqualTo(itemOne);
+        final PromptContentItemBulletedText realItemTwo =
+                (PromptContentItemBulletedText) realContentView.getListItems().get(1);
+        assertThat(realItemTwo.getText()).isEqualTo(itemTwo);
+
+    }
+
+    @Test
+    public void testPromptInfo_CanSetAndGetOptions_contentViewMoreOptionsButton() {
+        final String contentDescription = "test description";
+        final PromptContentViewWithMoreOptionsButton contentView =
+                new PromptContentViewWithMoreOptionsButton.Builder().setDescription(
+                        contentDescription).build();
+        final String title = "Title";
+        final String negativeButtonText = "Negative";
+
+        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
+                .setTitle(title)
+                .setNegativeButtonText(negativeButtonText)
+                .setContentView(contentView)
+                .build();
+
+        assertThat(info.getContentView()).isEqualTo(contentView);
+        assertThat(
+                ((PromptContentViewWithMoreOptionsButton) info.getContentView())
+                        .getDescription()).isEqualTo(contentDescription);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testPromptInfo_FailsToBuild_WithNoTitle() {
         new BiometricPrompt.PromptInfo.Builder().setNegativeButtonText("Cancel").build();
diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
index 51e0889..f935035 100644
--- a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
+++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
@@ -36,13 +36,13 @@
     }
 
     @Test
-    public void testCanUpdateLiveDataValue_OnMainThread() {
+    public void testCanUpdateNegativeButtonLiveDataValue_OnMainThread() {
         mViewModel.setNegativeButtonPressPending(true);
         assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
     }
 
     @Test
-    public void testCanUpdateLiveDataValue_OnBackgroundThread() throws Exception {
+    public void testCanUpdateNegativeButtonLiveDataValue_OnBackgroundThread() throws Exception {
         final Thread backgroundThread = new Thread(new Runnable() {
             @Override
             public void run() {
@@ -54,4 +54,24 @@
         ShadowLooper.runUiThreadTasks();
         assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
     }
+
+    @Test
+    public void testCanUpdateMoreOptionsButtonLiveDataValue_OnMainThread() {
+        mViewModel.setMoreOptionsButtonPressPending(true);
+        assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
+    }
+
+    @Test
+    public void testCanUpdateMoreOptionsButtonLiveDataValue_OnBackgroundThread() throws Exception {
+        final Thread backgroundThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                mViewModel.setMoreOptionsButtonPressPending(true);
+            }
+        });
+        backgroundThread.start();
+        backgroundThread.join();
+        ShadowLooper.runUiThreadTasks();
+        assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
+    }
 }
diff --git a/biometric/integration-tests/testapp/build.gradle b/biometric/integration-tests/testapp/build.gradle
index e36cafe..0b42c40 100644
--- a/biometric/integration-tests/testapp/build.gradle
+++ b/biometric/integration-tests/testapp/build.gradle
@@ -21,6 +21,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.biometric.integration.testapp"
     }
diff --git a/camera/camera-feature-combination-query-play-services/build.gradle b/camera/camera-feature-combination-query-play-services/build.gradle
index 721b314..de58db5 100644
--- a/camera/camera-feature-combination-query-play-services/build.gradle
+++ b/camera/camera-feature-combination-query-play-services/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api(libs.androidx.annotation)
+    project(":camera:camera-feature-combination-query")
     implementation(project(":camera:camera-feature-combination-query"))
 
     testImplementation(libs.testRunner)
@@ -32,9 +33,16 @@
     testImplementation(libs.truth)
     testImplementation(libs.testRules)
     testImplementation(libs.testCore)
+
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
 }
 
 android {
+    compileSdk 35
     lintOptions {
         enable 'CameraXQuirksClassDetector'
     }
@@ -53,4 +61,4 @@
             "library providing camera feature combination with Google Play Services dependencies."
     metalavaK2UastEnabled = true
     legacyDisableKotlinStrictApiMode = true
-}
\ No newline at end of file
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt b/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt
new file mode 100644
index 0000000..e2c097b
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/androidTest/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery.playservices
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.params.SessionConfiguration
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_PLAY_SERVICES
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 28)
+class PlayServicesCameraDeviceSetupCompatTest {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Test
+    fun queryResult_resultSourceIsPlayServices() {
+        // Arrange.
+        val factory = CameraDeviceSetupCompatFactory(instrumentation.context)
+        val impl = factory.getCameraDeviceSetupCompat("1")
+        val sessionConfiguration =
+            SessionConfiguration(
+                SessionConfiguration.SESSION_REGULAR,
+                listOf(),
+                directExecutor(),
+                object : CameraCaptureSession.StateCallback() {
+                    override fun onConfigured(p0: CameraCaptureSession) {
+                        // no-op
+                    }
+
+                    override fun onConfigureFailed(p0: CameraCaptureSession) {
+                        // no-op
+                    }
+                }
+            )
+        // Act.
+        val result = impl.isSessionConfigurationSupported(sessionConfiguration)
+        // Assert.
+        assertThat(result.source).isEqualTo(SOURCE_PLAY_SERVICES)
+        assertThat(result.supported).isEqualTo(SupportQueryResult.RESULT_UNSUPPORTED)
+        assertThat(result.timestampMillis).isEqualTo(0)
+    }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml b/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
index a5918e6..9a67ab2 100644
--- a/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
+++ b/camera/camera-feature-combination-query-play-services/src/main/AndroidManifest.xml
@@ -1,4 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <application>
+        <service
+            android:name="androidx.camera.featurecombinationquery.playservices.MetadataHolderService"
+            android:enabled="true"
+            android:exported="false"
+            tools:ignore="MissingServiceExportedEqualsTrue">
+            <meta-data
+                android:name="androidx.camera.featurecombinationquery.PLAY_SERVICES_IMPL_PROVIDER_KEY"
+                android:value="androidx.camera.featurecombinationquery.playservices.PlayServicesCameraDeviceSetupCompatProvider" />
+        </service>
+    </application>
 </manifest>
\ No newline at end of file
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java
new file mode 100644
index 0000000..48e84db
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/MetadataHolderService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery.playservices;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Service that holds metadata for the Play Services CameraDeviceSetup implementation.
+ */
+public class MetadataHolderService extends Service {
+
+    @Nullable
+    @Override
+    public IBinder onBind(@Nullable Intent intent) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java
new file mode 100644
index 0000000..9e53d6a
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompat.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery.playservices;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNSUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_PLAY_SERVICES;
+
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory;
+
+/**
+ * A Google Play Services based {@link CameraDeviceSetupCompat} implementation.
+ *
+ * <p>This class is internal only and app cannot instantiate it directly. Instead app will
+ * depend on the camera-feature-combination-query-play-services artifact to get an instance of
+ * this class via the {@link CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat} API.
+ */
+public class PlayServicesCameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+    public PlayServicesCameraDeviceSetupCompat(@NonNull String cameraId) {
+        // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+    }
+
+    @NonNull
+    @Override
+    public SupportQueryResult isSessionConfigurationSupported(
+            @NonNull SessionConfiguration sessionConfig) {
+        // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+        return new SupportQueryResult(RESULT_UNSUPPORTED, SOURCE_PLAY_SERVICES, 0);
+    }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..367ca54
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/PlayServicesCameraDeviceSetupCompatProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery.playservices;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompat;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatFactory;
+import androidx.camera.featurecombinationquery.CameraDeviceSetupCompatProvider;
+
+/**
+ * A Google Play Services based {@link CameraDeviceSetupCompat} implementation.
+ *
+ * <p>This class is internal only and app cannot instantiate it directly. Instead app will
+ * depend on the camera-feature-combination-query-play-services artifact to get an instance of
+ * this class via the {@link CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat} API.
+ */
+public class PlayServicesCameraDeviceSetupCompatProvider implements
+        CameraDeviceSetupCompatProvider {
+
+    public PlayServicesCameraDeviceSetupCompatProvider(@NonNull Context context) {
+        // TODO: Implement this once Google Play Services CameraDeviceSetup is available.
+    }
+
+    @NonNull
+    @Override
+    public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId) {
+        return new PlayServicesCameraDeviceSetupCompat(cameraId);
+    }
+}
diff --git a/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java
new file mode 100644
index 0000000..4d36705
--- /dev/null
+++ b/camera/camera-feature-combination-query-play-services/src/main/java/androidx/camera/featurecombinationquery/playservices/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.featurecombinationquery.playservices;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-feature-combination-query/api/1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
+++ b/camera/camera-feature-combination-query/api/1.4.0-beta03.txt
@@ -1 +1,30 @@
 // Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+  public interface CameraDeviceSetupCompat {
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  public static final class CameraDeviceSetupCompat.SupportQueryResult {
+    ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+    method public int getSource();
+    method public int getSupported();
+    method public long getTimestampMillis();
+    field public static final int RESULT_SUPPORTED = 1; // 0x1
+    field public static final int RESULT_UNDEFINED = 0; // 0x0
+    field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+    field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+    field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+    field public static final int SOURCE_UNDEFINED = 0; // 0x0
+  }
+
+  public class CameraDeviceSetupCompatFactory {
+    ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+  }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/current.txt b/camera/camera-feature-combination-query/api/current.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/current.txt
+++ b/camera/camera-feature-combination-query/api/current.txt
@@ -1 +1,30 @@
 // Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+  public interface CameraDeviceSetupCompat {
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  public static final class CameraDeviceSetupCompat.SupportQueryResult {
+    ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+    method public int getSource();
+    method public int getSupported();
+    method public long getTimestampMillis();
+    field public static final int RESULT_SUPPORTED = 1; // 0x1
+    field public static final int RESULT_UNDEFINED = 0; // 0x0
+    field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+    field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+    field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+    field public static final int SOURCE_UNDEFINED = 0; // 0x0
+  }
+
+  public class CameraDeviceSetupCompatFactory {
+    ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+  }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
+++ b/camera/camera-feature-combination-query/api/restricted_1.4.0-beta03.txt
@@ -1 +1,30 @@
 // Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+  public interface CameraDeviceSetupCompat {
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  public static final class CameraDeviceSetupCompat.SupportQueryResult {
+    ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+    method public int getSource();
+    method public int getSupported();
+    method public long getTimestampMillis();
+    field public static final int RESULT_SUPPORTED = 1; // 0x1
+    field public static final int RESULT_UNDEFINED = 0; // 0x0
+    field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+    field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+    field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+    field public static final int SOURCE_UNDEFINED = 0; // 0x0
+  }
+
+  public class CameraDeviceSetupCompatFactory {
+    ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+  }
+
+}
+
diff --git a/camera/camera-feature-combination-query/api/restricted_current.txt b/camera/camera-feature-combination-query/api/restricted_current.txt
index e6f50d0..fd3b5ae 100644
--- a/camera/camera-feature-combination-query/api/restricted_current.txt
+++ b/camera/camera-feature-combination-query/api/restricted_current.txt
@@ -1 +1,30 @@
 // Signature format: 4.0
+package @androidx.camera.featurecombinationquery.ExperimentalFeatureCombinationQuery androidx.camera.featurecombinationquery {
+
+  public interface CameraDeviceSetupCompat {
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult isSessionConfigurationSupported(android.hardware.camera2.params.SessionConfiguration) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  public static final class CameraDeviceSetupCompat.SupportQueryResult {
+    ctor public CameraDeviceSetupCompat.SupportQueryResult(int, int, long);
+    method public int getSource();
+    method public int getSupported();
+    method public long getTimestampMillis();
+    field public static final int RESULT_SUPPORTED = 1; // 0x1
+    field public static final int RESULT_UNDEFINED = 0; // 0x0
+    field public static final int RESULT_UNSUPPORTED = 2; // 0x2
+    field public static final int SOURCE_ANDROID_FRAMEWORK = 2; // 0x2
+    field public static final int SOURCE_PLAY_SERVICES = 1; // 0x1
+    field public static final int SOURCE_UNDEFINED = 0; // 0x0
+  }
+
+  public class CameraDeviceSetupCompatFactory {
+    ctor public CameraDeviceSetupCompatFactory(android.content.Context);
+    method public androidx.camera.featurecombinationquery.CameraDeviceSetupCompat getCameraDeviceSetupCompat(String) throws android.hardware.camera2.CameraAccessException;
+  }
+
+  @SuppressCompatibility @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @kotlin.RequiresOptIn public @interface ExperimentalFeatureCombinationQuery {
+  }
+
+}
+
diff --git a/camera/camera-feature-combination-query/build.gradle b/camera/camera-feature-combination-query/build.gradle
index 12d1d72..6d13031 100644
--- a/camera/camera-feature-combination-query/build.gradle
+++ b/camera/camera-feature-combination-query/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api(libs.androidx.annotation)
+    implementation(libs.autoValueAnnotations)
 
     testImplementation(libs.testRunner)
     testImplementation(libs.robolectric)
@@ -31,6 +32,8 @@
     testImplementation(libs.truth)
     testImplementation(libs.testRules)
     testImplementation(libs.testCore)
+
+    annotationProcessor(libs.autoValue)
 }
 
 android {
@@ -38,6 +41,7 @@
         enable 'CameraXQuirksClassDetector'
     }
 
+    compileSdk 35
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.camera.featurecombinationquery"
 }
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java
new file mode 100644
index 0000000..6fc6e84
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/AggregatedCameraDeviceSetupCompat.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNDEFINED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_UNDEFINED;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * A {@link CameraDeviceSetupCompat} implementation that combines multiple
+ * {@link CameraDeviceSetupCompat}.
+ *
+ * <p>This class checks if a {@link SessionConfiguration} is supported in the order of the
+ * provided implementation list, and returns the first non-undefined result. If all results are
+ * undefined, it will return a undefined result.
+ */
+final class AggregatedCameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+    private final List<CameraDeviceSetupCompat> mCameraDeviceSetupImpls;
+
+    AggregatedCameraDeviceSetupCompat(List<CameraDeviceSetupCompat> cameraDeviceSetupImpls) {
+        mCameraDeviceSetupImpls = cameraDeviceSetupImpls;
+    }
+
+    @NonNull
+    @Override
+    public SupportQueryResult isSessionConfigurationSupported(
+            @NonNull SessionConfiguration sessionConfig)
+            throws CameraAccessException {
+        for (CameraDeviceSetupCompat impl : mCameraDeviceSetupImpls) {
+            SupportQueryResult result = impl.isSessionConfigurationSupported(sessionConfig);
+            if (result.getSupported() != RESULT_UNDEFINED) {
+                return result;
+            }
+        }
+        return new SupportQueryResult(RESULT_UNDEFINED, SOURCE_UNDEFINED, 0);
+    }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java
new file mode 100644
index 0000000..dc4fce8
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompat.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_SUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.RESULT_UNSUPPORTED;
+import static androidx.camera.featurecombinationquery.CameraDeviceSetupCompat.SupportQueryResult.SOURCE_ANDROID_FRAMEWORK;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A Android framework based {@link CameraDeviceSetupCompat} implementation.
+ */
+@RequiresApi(api = 35)
+class Camera2CameraDeviceSetupCompat implements CameraDeviceSetupCompat {
+
+    private final CameraDevice.CameraDeviceSetup mCameraDeviceSetup;
+
+    Camera2CameraDeviceSetupCompat(@NonNull CameraManager cameraManager, @NonNull String cameraId)
+            throws CameraAccessException {
+        mCameraDeviceSetup = cameraManager.getCameraDeviceSetup(cameraId);
+    }
+
+    @NonNull
+    @Override
+    public SupportQueryResult isSessionConfigurationSupported(
+            @NonNull SessionConfiguration sessionConfig)
+            throws CameraAccessException {
+        return new SupportQueryResult(
+                mCameraDeviceSetup.isSessionConfigurationSupported(sessionConfig) ? RESULT_SUPPORTED
+                        : RESULT_UNSUPPORTED,
+                SOURCE_ANDROID_FRAMEWORK,
+                getBuildTimeEpochMillis());
+    }
+
+    public static long getBuildTimeEpochMillis() {
+        String value = System.getProperty("ro.build.date.utc");
+        if (value != null) {
+            try {
+                return Long.parseLong(value) * 1000;
+            } catch (NumberFormatException e) {
+                // Fall through
+            }
+        }
+        return 0;
+    }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..aa8eceb
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/Camera2CameraDeviceSetupCompatProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A Android framework based {@link CameraDeviceSetupCompat} implementation.
+ */
+@RequiresApi(api = 35)
+class Camera2CameraDeviceSetupCompatProvider implements CameraDeviceSetupCompatProvider {
+
+    private final CameraManager mCameraManager;
+
+    Camera2CameraDeviceSetupCompatProvider(@NonNull Context context) {
+        mCameraManager = context.getSystemService(CameraManager.class);
+    }
+
+    @NonNull
+    @Override
+    public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+            throws CameraAccessException {
+        return new Camera2CameraDeviceSetupCompat(mCameraManager, cameraId);
+    }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java
new file mode 100644
index 0000000..e856e7e
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompat.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Interface for checking if a {@link SessionConfiguration} is supported by the device.
+ *
+ * <p>This interface is a compatible version of the {@link CameraDevice.CameraDeviceSetup}
+ * class.
+ *
+ * <p>Implementations of this interface must be able to check if a {@link SessionConfiguration}
+ * is supported. They will check both the output streams and the session parameters, then return
+ * whether the combination works for the given camera. For example, a camera device may support
+ * HDR and 60FPS frame rate, but not both at the same time.
+ *
+ * @see CameraDevice.CameraDeviceSetup
+ */
+public interface CameraDeviceSetupCompat {
+
+    /**
+     * Checks if the {@link SessionConfiguration} is supported.
+     *
+     * @param sessionConfig The {@link SessionConfiguration} to check.
+     * @return a {@link SupportQueryResult} indicating if the {@link SessionConfiguration} is
+     * supported.
+     * @throws CameraAccessException if the camera device is no longer connected or has
+     *                               encountered a fatal error.
+     * @see CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported
+     */
+    @NonNull
+    SupportQueryResult isSessionConfigurationSupported(@NonNull SessionConfiguration sessionConfig)
+            throws CameraAccessException;
+
+    /**
+     * Result of a {@link CameraDeviceSetupCompat#isSessionConfigurationSupported} query.
+     */
+    final class SupportQueryResult {
+
+        /**
+         * Source of the result is undefined. This is always accompanied by
+         * {@link #RESULT_UNDEFINED}.
+         */
+        public static final int SOURCE_UNDEFINED = 0;
+        /**
+         * Source of the result is Google Play Services.
+         */
+        public static final int SOURCE_PLAY_SERVICES = 1;
+        /**
+         * Source of the result is Android framework.
+         */
+        public static final int SOURCE_ANDROID_FRAMEWORK = 2;
+
+        /**
+         * The library cannot determine if the {@link SessionConfiguration} is supported.
+         *
+         * <p>For API levels 29 to 34 inclusive, the app may continue to call
+         * {@link CameraDevice#isSessionConfigurationSupported} to check if the session
+         * configuration is supported.
+         *
+         * @see CameraDeviceSetupCompatFactory#getCameraDeviceSetupCompat for sample code.
+         */
+        public static final int RESULT_UNDEFINED = 0;
+        /**
+         * The {@link SessionConfiguration} is supported by the camera.
+         */
+        public static final int RESULT_SUPPORTED = 1;
+        /**
+         * The {@link SessionConfiguration} is not supported by the camera.
+         */
+        public static final int RESULT_UNSUPPORTED = 2;
+
+        /**
+         * Options for where the result is coming from.
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @IntDef(value = {RESULT_UNDEFINED, RESULT_SUPPORTED, RESULT_UNSUPPORTED})
+        public @interface Supported {
+        }
+
+        /**
+         * Options for where the result is coming from.
+         */
+        @Retention(RetentionPolicy.SOURCE)
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @IntDef(value = {SOURCE_UNDEFINED, SOURCE_PLAY_SERVICES, SOURCE_ANDROID_FRAMEWORK})
+        @interface Sources {
+        }
+
+        // Whether the configuration is supported.
+        @Supported
+        private final int mSupported;
+        // The source of the result.
+        @Sources
+        private final int mSource;
+        // The timestamp of when the result was updated.
+        private final long mTimestampMillis;
+
+        /**
+         * Creates a new instance of {@link SupportQueryResult}.
+         *
+         * @param supported       Whether the {@link SessionConfiguration} is supported.
+         * @param source          The source of the result.
+         * @param timestampMillis The epoch timestamp of when the result was updated.
+         */
+        public SupportQueryResult(int supported, int source, long timestampMillis) {
+            mSupported = supported;
+            mSource = source;
+            mTimestampMillis = timestampMillis;
+        }
+
+        /**
+         * Whether the {@link SessionConfiguration} is supported.
+         *
+         * <p> If the value is {@link #RESULT_SUPPORTED}, the configuration is
+         * supported by the camera; if the value is {@link #RESULT_UNSUPPORTED}, the
+         * configuration is not supported by the camera; if the value is
+         * {@link #RESULT_UNDEFINED}, then the library cannot determine if the configuration is
+         * supported or not.
+         */
+        @Supported
+        public int getSupported() {
+            return mSupported;
+        }
+
+        /**
+         * Returns the source of the result.
+         *
+         * <p> If the source of the information is Play Services, the value is
+         * {@link #SOURCE_PLAY_SERVICES}; if the source is Android framework, the value is
+         * {@link #SOURCE_ANDROID_FRAMEWORK}; otherwise, the value is {@link #SOURCE_UNDEFINED}.
+         */
+        @Sources
+        public int getSource() {
+            return mSource;
+        }
+
+        /**
+         * Returns the epoch timestamp of when the result was updated.
+         *
+         * <p> If the source is {@link #SOURCE_PLAY_SERVICES}, the value is the time when the
+         * data is updated on the Play Services server; if the source is
+         * {@link #SOURCE_ANDROID_FRAMEWORK}, the value is the build property "ro.build.date.utc"
+         * if available; otherwise, it will return 0.
+         */
+        public long getTimestampMillis() {
+            return mTimestampMillis;
+        }
+    }
+
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java
new file mode 100644
index 0000000..34fb0d0
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatFactory.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.SessionConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory for creating {@link CameraDeviceSetupCompat} instances.
+ */
+public class CameraDeviceSetupCompatFactory {
+
+    private static final String PLAY_SERVICES_IMPL_KEY =
+            "androidx.camera.featurecombinationquery.PLAY_SERVICES_IMPL_PROVIDER_KEY";
+
+    private final Context mContext;
+
+    // Cached provider for Play Services implementation.
+    @Nullable
+    private CameraDeviceSetupCompatProvider mPlayServicesProvider;
+    // Cached provider for Camera2 implementation.
+    @Nullable
+    private CameraDeviceSetupCompatProvider mCamera2Provider;
+
+    /**
+     * Creates a new instance of {@link CameraDeviceSetupCompatFactory}.
+     *
+     * @param context The context to use for creating {@link CameraDeviceSetupCompat} instances.
+     */
+    public CameraDeviceSetupCompatFactory(@NonNull Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Gets a new instance of {@link CameraDeviceSetupCompat} for the given camera ID.
+     *
+     * <p> The returned instance aggregates the results from both the Play Services and the
+     * Android framework. It first checks if a Play Services implementation exists, and if so,
+     * return the query result from the Play Services implementation. If no Play Services
+     * implementation exists or the result from Play Services is undefined, the returned instance
+     * will then query Android framework for the result, if running on a new enough version.
+     * Sample code:
+     *
+     * <pre><code>
+     * // Query the compatibility before opening the camera.
+     * CameraDeviceSetupCompatFactory factory = new CameraDeviceSetupCompatFactory(context);
+     * CameraDeviceSetupCompat cameraDeviceSetupCompat
+     *     = factory.getCameraDeviceSetupCompat(cameraId);
+     * Result result
+     *     = cameraDeviceSetupCompat.isSessionConfigurationSupported(sessionConfiguration);
+     * boolean supported = result.getValue() == CameraDeviceSetupCompat.RESULT_SUPPORTED;
+     * </code></pre>
+     *
+     * <p> To include the Play Services as a source, the app must depend on the
+     * camera-feature-combination-query-play-services artifact.
+     *
+     * <p> If the return value is
+     * {@link CameraDeviceSetupCompat.SupportQueryResult#RESULT_UNDEFINED}, on API level between
+     * 29 and 34 inclusive, it's also possible to further check if the session
+     * configuration is supported by querying the
+     * {@link CameraDevice#isSessionConfigurationSupported}. This approach requires
+     * opening the camera first which may introduce latency, so the AndroidX implementation does not
+     * include this code path by default. Additionally, this approach does not check the values
+     * set in {@link SessionConfiguration#setSessionParameters}. For example, the FPS range is
+     * ignored. Sample code:
+     *
+     * <pre><code>
+     * if (SDK_INT <= 34 && SDK_INT >= 29) {
+     *   // Check if the session configuration is supported with an opened CameraDevice.
+     *   try {
+     *     supported = supported || (result.getValue() == RESULT_UNDEFINED &&
+     *         cameraDevice.isSessionConfigurationSupported(sessionConfiguration));
+     *   } catch (UnsupportedOperationException unsupportedException) {
+     *     // CameraDevice may throw UnsupportedOperationException when the config is not supported.
+     *   }
+     * }
+     * </code></pre>
+     *
+     * @throws IllegalStateException    If the Play Services implementation exists but the library
+     *                                  fails to instantiate it. For example, if there are multiple
+     *                                  Play Services implementations in the manifest.
+     * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not
+     *                                  match any device in {@link CameraManager#getCameraIdList()}.
+     * @throws CameraAccessException    If the camera has encountered a fatal error.
+     * @see CameraDevice#isSessionConfigurationSupported
+     * @see CameraDevice.CameraDeviceSetup#isSessionConfigurationSupported
+     */
+    @NonNull
+    public CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+            throws CameraAccessException {
+        List<CameraDeviceSetupCompat> impls = new ArrayList<>();
+        if (mPlayServicesProvider == null) {
+            // Create Play Services implementation if there isn't a cached one.
+            mPlayServicesProvider = getPlayServicesCameraDeviceSetupCompatProvider();
+        }
+        if (mPlayServicesProvider != null) {
+            // Add the Play Services implementation if the app contains that dependency.
+            impls.add(mPlayServicesProvider.getCameraDeviceSetupCompat(cameraId));
+        }
+        if (SDK_INT >= 35) {
+            try {
+                if (mCamera2Provider == null) {
+                    // Create the camera2 implementation if there isn't a cached one.
+                    mCamera2Provider = new Camera2CameraDeviceSetupCompatProvider(mContext);
+                }
+                impls.add(mCamera2Provider.getCameraDeviceSetupCompat(cameraId));
+            } catch (UnsupportedOperationException e) {
+                // This can throw UnsupportedOperationException for Android V upgrade devices. In
+                // that case, we treat it as SDK_INT < 35 and ignore.
+            }
+        }
+        return new AggregatedCameraDeviceSetupCompat(impls);
+    }
+
+    /**
+     * Returns a new instance of {@link CameraDeviceSetupCompatProvider}.
+     *
+     * @return The Play Services CameraDeviceSetupCompat implementation, or null if not found.
+     * @throws IllegalStateException if multiple Play Services CameraDeviceSetupCompat
+     *                               implementations are found, or failed to instantiate the
+     *                               implementation.
+     */
+    @Nullable
+    private CameraDeviceSetupCompatProvider getPlayServicesCameraDeviceSetupCompatProvider()
+            throws IllegalStateException {
+        PackageInfo packageInfo;
+        try {
+            packageInfo = mContext.getPackageManager().getPackageInfo(
+                    mContext.getPackageName(),
+                    PackageManager.GET_META_DATA | PackageManager.GET_SERVICES);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+
+        String playServiceImplClassName = null;
+        if (packageInfo.services == null) {
+            return null;
+        }
+        for (ServiceInfo serviceInfo : packageInfo.services) {
+            if (serviceInfo.metaData == null) {
+                continue;
+            }
+            // Try to load the play services impl class name from the Service metadata
+            // in camera-feature-combination-query-play-services manifest.
+            String className = serviceInfo.metaData.getString(PLAY_SERVICES_IMPL_KEY);
+            if (className != null) {
+                if (playServiceImplClassName != null) {
+                    throw new IllegalStateException(
+                            "Multiple Play Services CameraDeviceSetupCompat implementations"
+                                    + " found in the manifest.");
+                }
+                playServiceImplClassName = className;
+            }
+        }
+        if (playServiceImplClassName == null) {
+            // The app does not depend on a Play Services implementation.
+            return null;
+        }
+        return instantiatePlayServicesImplProvider(playServiceImplClassName);
+    }
+
+    /**
+     * Instantiates a Play Services CameraDeviceSetupCompat provider implementation.
+     *
+     * @param className The class name of the implementation.
+     * @return The instantiated implementation.
+     */
+    private CameraDeviceSetupCompatProvider instantiatePlayServicesImplProvider(
+            @NonNull String className) {
+        try {
+            Class<?> clazz = Class.forName(className);
+            return (CameraDeviceSetupCompatProvider) clazz.getConstructor(Context.class)
+                    .newInstance(mContext);
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                    "Failed to instantiate Play Services CameraDeviceSetupCompat implementation",
+                    e);
+        }
+    }
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java
new file mode 100644
index 0000000..b61d909
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/CameraDeviceSetupCompatProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import android.hardware.camera2.CameraAccessException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Interface for providing a {@link CameraDeviceSetupCompat} for a camera device.
+ *
+ * <p> Getting a provider usually needs Binder calls which is costly. This interface allows the
+ * to be cached and reused.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface CameraDeviceSetupCompatProvider {
+
+    /**
+     * Get a {@link CameraDeviceSetupCompat} for the camera device with the given cameraId.
+     *
+     * @param cameraId the cameraId of the camera device.
+     * @return a {@link CameraDeviceSetupCompat} for the camera device with the given cameraId.
+     */
+    @NonNull
+    CameraDeviceSetupCompat getCameraDeviceSetupCompat(@NonNull String cameraId)
+            throws CameraAccessException;
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java
new file mode 100644
index 0000000..cf686ce
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/ExperimentalFeatureCombinationQuery.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.camera.featurecombinationquery;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import kotlin.RequiresOptIn;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Indicates that the annotated API uses the experimental feature combination query API.
+ *
+ * <p> This feature will move to official API in 1.5.0.
+ */
+@Retention(CLASS)
+@RequiresOptIn
+public @interface ExperimentalFeatureCombinationQuery {
+}
diff --git a/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java
new file mode 100644
index 0000000..fdf1063
--- /dev/null
+++ b/camera/camera-feature-combination-query/src/main/java/androidx/camera/featurecombinationquery/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@ExperimentalFeatureCombinationQuery
+package androidx.camera.featurecombinationquery;
+
diff --git a/camera/integration-tests/avsynctestapp/build.gradle b/camera/integration-tests/avsynctestapp/build.gradle
index 5e5ce6c..870cf0f 100644
--- a/camera/integration-tests/avsynctestapp/build.gradle
+++ b/camera/integration-tests/avsynctestapp/build.gradle
@@ -24,6 +24,8 @@
 android {
     namespace 'androidx.camera.integration.avsync'
 
+    compileSdk 35
+
     defaultConfig {
         applicationId "androidx.camera.integration.avsync"
     }
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 453f43f..33c414d 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -21,6 +21,8 @@
 }
 
 android {
+    compileSdk 35
+
     defaultConfig {
         applicationId "androidx.camera.integration.uiwidgets"
         versionCode 1
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 55f2e64e0..dfad750 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -127,5 +127,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.core"
 }
diff --git a/compose/animation/animation-core/samples/build.gradle b/compose/animation/animation-core/samples/build.gradle
index f566a97..2a21612 100644
--- a/compose/animation/animation-core/samples/build.gradle
+++ b/compose/animation/animation-core/samples/build.gradle
@@ -51,5 +51,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.core.samples"
 }
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index d384dff..3398b9f 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -121,5 +121,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.graphics"
 }
diff --git a/compose/animation/animation-graphics/samples/build.gradle b/compose/animation/animation-graphics/samples/build.gradle
index c794e59..dd05f1e 100644
--- a/compose/animation/animation-graphics/samples/build.gradle
+++ b/compose/animation/animation-graphics/samples/build.gradle
@@ -52,5 +52,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.graphics.samples"
 }
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 216eb4f..5e9f178 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -133,5 +133,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation"
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index a826c38..a146eb3 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -40,5 +40,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.demos"
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
index 7534afc..228b929 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
@@ -77,6 +77,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.times
 import androidx.compose.ui.window.Popup
+import kotlin.collections.removeLast as removeLastKt
 import kotlin.math.atan2
 import kotlin.math.cos
 import kotlin.math.log
@@ -309,7 +310,7 @@
     fun removeAnchor() {
         if (anchors.size > 1) {
             scope.launch { animatedOffset.snapTo(Offset.Zero) }
-            anchors.removeLast()
+            anchors.removeLastKt()
             modificationIndicator++
         }
     }
diff --git a/compose/animation/animation/samples/build.gradle b/compose/animation/animation/samples/build.gradle
index d7aecde..3d7e32c 100644
--- a/compose/animation/animation/samples/build.gradle
+++ b/compose/animation/animation/samples/build.gradle
@@ -50,5 +50,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.animation.samples"
 }
diff --git a/compose/benchmark-utils/build.gradle b/compose/benchmark-utils/build.gradle
index 7e3d1ab..b994260 100644
--- a/compose/benchmark-utils/build.gradle
+++ b/compose/benchmark-utils/build.gradle
@@ -56,10 +56,11 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.benchmarkutils"
 
     // workarounds for b/328649293
     buildTypes.configureEach {
         consumerProguardFiles "proguard-rules.pro"
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index b68d2b4..cc742f9 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -119,5 +119,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.foundation.layout"
 }
diff --git a/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle b/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
index e9a5acb..122b991 100644
--- a/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
+++ b/compose/foundation/foundation-layout/integration-tests/layout-demos/build.gradle
@@ -36,5 +36,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.foundation.layout.demos"
 }
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index 8718492..7b33988 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -53,5 +53,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.foundation.layout.samples"
 }
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 4544b8d..7a84faa 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -137,6 +137,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/foundation/foundation"
     namespace "androidx.compose.foundation"
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
index 063a475..cc5cc22 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/build.gradle
@@ -47,5 +47,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.foundation.demos"
 }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
index 6e32f3d..504de76 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/DrawTextDemo.kt
@@ -72,6 +72,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import kotlin.collections.removeFirst as removeFirstKt
 import kotlin.math.roundToInt
 import kotlin.math.roundToLong
 import kotlin.system.measureNanoTime
@@ -453,7 +454,7 @@
     fun addMeasure(duration: Long) {
         values.add(duration)
         while (values.size > capacity) {
-            values.removeFirst()
+            values.removeFirstKt()
         }
     }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
index aa3cf8b..4eb96a7 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldFocusDemo.kt
@@ -58,6 +58,7 @@
 import androidx.compose.ui.input.key.type
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.dp
+import kotlin.collections.removeLast as removeLastKt
 
 private val modifierKeys =
     setOf(
@@ -100,7 +101,7 @@
         if (keys.none { it.keyEvent.keyCode == event.keyCode && !it.isUp }) {
             keys.add(0, KeyState(event))
             if (keys.size > 10) {
-                keys.removeLast()
+                keys.removeLastKt()
             }
         }
     }
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 9308a13..a2e7565 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -55,6 +55,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.foundation.samples"
     // TODO(b/328001575)
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
index 182f9dd..0cf9953 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerPinnableContainerTest.kt
@@ -42,6 +42,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.collections.removeFirst as removeFirstKt
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Rule
@@ -475,7 +476,7 @@
         while (handles.isNotEmpty()) {
             rule.runOnIdle {
                 assertThat(composed).contains(1)
-                handles.removeFirst().release()
+                handles.removeFirstKt().release()
             }
         }
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
index c6f42a1..4db7891a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextHoverTest.kt
@@ -51,6 +51,7 @@
 class BasicTextHoverTest {
     @get:Rule val rule = createComposeRule()
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSelectableText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runSelectableTest(
@@ -60,6 +61,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSelectableText_andSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runSelectableTest(
@@ -69,6 +71,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSelectableText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
         runSelectableTest(
@@ -135,6 +138,7 @@
             }
         }
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenNonSelectableText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runNonSelectableTest(
@@ -144,6 +148,7 @@
             expectedTextIcon = TYPE_DEFAULT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenNonSelectableText_andSetIcon_inBoxWithDefaultIcon_setIconIsUsed() =
         runNonSelectableTest(
@@ -153,6 +158,7 @@
             expectedTextIcon = TYPE_CROSSHAIR
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenNonSelectableText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
         runNonSelectableTest(
@@ -217,6 +223,7 @@
             }
         }
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenDisabledSelectionText_andDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runDisabledSelectionText(
@@ -226,6 +233,7 @@
             expectedTextIcon = TYPE_DEFAULT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenDisabledSelectionText_andSetIcon_inBoxWithDefaultIcon_setIconIsUsed() =
         runDisabledSelectionText(
@@ -235,6 +243,7 @@
             expectedTextIcon = TYPE_CROSSHAIR
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenDisabledSelectionText_andSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
         runDisabledSelectionText(
@@ -337,6 +346,6 @@
 
             // Exit hovering over element
             rule.onNodeWithTag(selectionContainerTag).performMouseInput { exit() }
-            assertIcon(TYPE_DEFAULT)
+            @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
         }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
index 5a27adf..7fd6761 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHoverTest.kt
@@ -52,6 +52,7 @@
 class CoreTextFieldHoverTest {
     @get:Rule val rule = createComposeRule()
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runTest(
@@ -61,6 +62,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runTest(
@@ -70,6 +72,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
         runTest(
@@ -148,6 +151,6 @@
 
             // Exit hovering over element
             rule.onNodeWithTag(boxTag).performMouseInput { exit() }
-            assertIcon(TYPE_DEFAULT)
+            @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
         }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
index ce78be4..d01acf3 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/FakeInputMethodManager.kt
@@ -21,12 +21,13 @@
 import android.view.inputmethod.ExtractedText
 import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
 import com.google.common.truth.Truth.assertThat
+import kotlin.collections.removeFirst as removeFirstKt
 
 internal open class FakeInputMethodManager : ComposeInputMethodManager {
     private val calls = mutableListOf<String>()
 
     fun expectCall(description: String) {
-        assertThat(calls.removeFirst()).isEqualTo(description)
+        assertThat(calls.removeFirstKt()).isEqualTo(description)
     }
 
     fun expectNoMoreCalls() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
index e873706..5c1cbcc 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/BasicTextFieldHoverTest.kt
@@ -51,6 +51,7 @@
 class BasicTextFieldHoverTest {
     @get:Rule val rule = createComposeRule()
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenDefaultIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runTest(
@@ -60,6 +61,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSetIcon_inBoxWithDefaultIcon_textIconIsUsed() =
         runTest(
@@ -69,6 +71,7 @@
             expectedTextIcon = TYPE_TEXT
         )
 
+    @Suppress("DEPRECATION")
     @Test
     fun whenSetIcon_withOverride_inBoxWithDefaultIcon_setIconIsUsed() =
         runTest(
@@ -146,6 +149,6 @@
 
             // Exit hovering over element
             rule.onNodeWithTag(boxTag).performMouseInput { exit() }
-            assertIcon(TYPE_DEFAULT)
+            @Suppress("DEPRECATION") assertIcon(TYPE_DEFAULT)
         }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
index bebdcbd..a1e4a61 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/matchers/BitmapSubject.kt
@@ -56,7 +56,7 @@
 
     override fun actualCustomStringRepresentation(): String {
         return if (subject != null) {
-            "($subject ${subject.width}x${subject.height} ${subject.config.name})"
+            "($subject ${subject.width}x${subject.height} ${subject.config!!.name})"
         } else {
             super.actualCustomStringRepresentation()
         }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
index b54b8b8..1c50185 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/undo/UndoManager.kt
@@ -20,6 +20,8 @@
 import androidx.compose.runtime.saveable.SaverScope
 import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.ui.util.fastForEach
+import kotlin.collections.removeFirst as removeFirstKt
+import kotlin.collections.removeLast as removeLastKt
 
 /**
  * A generic purpose undo/redo stack manager.
@@ -60,7 +62,7 @@
         redoStack.clear()
 
         while (size > capacity - 1) { // leave room for the immediate `add`
-            undoStack.removeFirst()
+            undoStack.removeFirstKt()
         }
         undoStack.add(undoableAction)
     }
@@ -77,7 +79,7 @@
                 "Please first check `canUndo` value before calling the `undo` function."
         }
 
-        val topOperation = undoStack.removeLast()
+        val topOperation = undoStack.removeLastKt()
 
         redoStack.add(topOperation)
         return topOperation
@@ -95,7 +97,7 @@
                 "Please first check `canRedo` value before calling the `redo` function."
         }
 
-        val topOperation = redoStack.removeLast()
+        val topOperation = redoStack.removeLastKt()
 
         undoStack.add(topOperation)
         return topOperation
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 7d2fed8..8622500 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -46,5 +46,6 @@
 ApkCopyHelperKt.setupAppApkCopy(project, "release")
 
 android {
+    compileSdk 35
     namespace "androidx.compose.integration.demos"
 }
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index 5e67574..a663c70 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -161,7 +161,9 @@
     SideEffect {
         WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !isDarkMode
         WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !isDarkMode
+        @Suppress("deprecation")
         window.statusBarColor = Color.Transparent.toArgb()
+        @Suppress("deprecation")
         window.navigationBarColor = Color.Transparent.toArgb()
     }
     MaterialTheme(colorScheme = colorScheme, content = content)
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index e3ff228..7b9584b 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -70,6 +70,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.integration.docs"
 }
 
diff --git a/compose/integration-tests/hero/hero-implementation/build.gradle b/compose/integration-tests/hero/hero-implementation/build.gradle
index 690088e..2543f87 100644
--- a/compose/integration-tests/hero/hero-implementation/build.gradle
+++ b/compose/integration-tests/hero/hero-implementation/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdk 35
     namespace 'androidx.compose.integration.hero.implementation'
 
     lint {
@@ -59,4 +60,4 @@
     implementation(project(":compose:runtime:runtime-tracing"))
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-tooling"))
-}
\ No newline at end of file
+}
diff --git a/compose/integration-tests/hero/macrobenchmark-target/build.gradle b/compose/integration-tests/hero/macrobenchmark-target/build.gradle
index 181d642..acb2d78b 100644
--- a/compose/integration-tests/hero/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/hero/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "androidx.compose.integration.hero.macrobenchmark.target"
 
     buildTypes {
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 5946d76..a4cf022 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -13,6 +13,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.integration.macrobenchmark.target"
     buildTypes {
         release {
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index 973f875..31e34a8 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -32,6 +32,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.compose.material.catalog"
         versionCode 2400
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
index 0ce9ddb..26c903c 100644
--- a/compose/material/material-navigation/build.gradle
+++ b/compose/material/material-navigation/build.gradle
@@ -48,6 +48,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.navigation"
     // TODO(b/349411310)?
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/material/material-navigation/samples/build.gradle b/compose/material/material-navigation/samples/build.gradle
index b0a9deb..20b3d14d 100644
--- a/compose/material/material-navigation/samples/build.gradle
+++ b/compose/material/material-navigation/samples/build.gradle
@@ -48,5 +48,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.navigation.samples"
 }
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 77af4eb..2fa1de4 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -113,5 +113,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.ripple"
 }
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 2a7e5d6..724fa3f 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -142,6 +142,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/material/material"
     namespace "androidx.compose.material"
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index ba6ac0a..31c9cef 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -49,5 +49,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.catalog.library"
 }
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index c51c55c..a9bb660 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -37,5 +37,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.demos"
 }
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
index 04dac12..69dc9d9 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
@@ -64,7 +64,9 @@
         setContent {
             val palette = interpolateTheme(scrollFraction.floatValue)
             val darkenedPrimary = palette.darkenedPrimary
+            @Suppress("DEPRECATION")
             window.statusBarColor = darkenedPrimary
+            @Suppress("DEPRECATION")
             window.navigationBarColor = darkenedPrimary
 
             DynamicThemeApp(scrollFraction, palette)
diff --git a/compose/material/material/samples/build.gradle b/compose/material/material/samples/build.gradle
index 4f48d58..8ec0f0d 100644
--- a/compose/material/material/samples/build.gradle
+++ b/compose/material/material/samples/build.gradle
@@ -54,6 +54,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material.samples"
     // TODO(b/328001575)
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index bc2a2d1..e4d441b 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -101,10 +101,6 @@
     }
 }
 
-android {
-    namespace "androidx.compose.material3.adaptive.layout"
-}
-
 androidx {
     name = "Material Adaptive"
     type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
@@ -120,6 +116,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
     namespace "androidx.compose.material3.adaptive.layout"
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index 9c75325..001bd0a 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -114,6 +114,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
     namespace "androidx.compose.material3.adaptive.navigation"
diff --git a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
index 10a5006..3feed63 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigation/ThreePaneScaffoldNavigator.kt
@@ -42,6 +42,7 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.util.fastMap
+import kotlin.collections.removeLast as removeLastKt
 
 /**
  * The common interface of the default navigation implementations for different three-pane
@@ -344,7 +345,7 @@
         }
         val targetSize = previousDestinationIndex + 1
         while (destinationHistory.size > targetSize) {
-            destinationHistory.removeLast()
+            destinationHistory.removeLastKt()
         }
         return true
     }
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 2289202..ecc8e46 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -114,6 +114,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/material3/adaptive"
     namespace "androidx.compose.material3.adaptive"
diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
index 18b0996..bf9047b 100644
--- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
+++ b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
@@ -83,7 +83,10 @@
 internal class MockWindowMetricsCalculator(private val mockWindowSize: State<IntSize>) :
     WindowMetricsCalculator {
     override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
-        return WindowMetrics(Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height))
+        return WindowMetrics(
+            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
+            density = 1f
+        )
     }
 
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
@@ -91,6 +94,9 @@
     }
 
     override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
-        return WindowMetrics(Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height))
+        return WindowMetrics(
+            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
+            density = 1f
+        )
     }
 }
diff --git a/compose/material3/integration-tests/macrobenchmark-target/build.gradle b/compose/material3/integration-tests/macrobenchmark-target/build.gradle
index 087ceb2..c19b92d 100644
--- a/compose/material3/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/material3/integration-tests/macrobenchmark-target/build.gradle
@@ -14,9 +14,10 @@
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
         }
     }
+    compileSdkVersion 35
 }
 
 dependencies {
     implementation(project(":activity:activity-compose"))
     implementation(project(":compose:material3:material3"))
-}
\ No newline at end of file
+}
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/current.txt b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
index 83d23c0..62a3e0d 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/current.txt
@@ -59,10 +59,12 @@
   }
 
   public static final class NavigationSuiteType.Companion {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public String getCustom();
     method public String getNavigationBar();
     method public String getNavigationDrawer();
     method public String getNavigationRail();
     method public String getNone();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final String Custom;
     property public final String NavigationBar;
     property public final String NavigationDrawer;
     property public final String NavigationRail;
diff --git a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
index 83d23c0..62a3e0d 100644
--- a/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive-navigation-suite/api/restricted_current.txt
@@ -59,10 +59,12 @@
   }
 
   public static final class NavigationSuiteType.Companion {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public String getCustom();
     method public String getNavigationBar();
     method public String getNavigationDrawer();
     method public String getNavigationRail();
     method public String getNone();
+    property @SuppressCompatibility @androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi public final String Custom;
     property public final String NavigationBar;
     property public final String NavigationDrawer;
     property public final String NavigationRail;
diff --git a/compose/material3/material3-adaptive-navigation-suite/build.gradle b/compose/material3/material3-adaptive-navigation-suite/build.gradle
index 83a6e86..1f731d6 100644
--- a/compose/material3/material3-adaptive-navigation-suite/build.gradle
+++ b/compose/material3/material3-adaptive-navigation-suite/build.gradle
@@ -96,6 +96,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material3.adaptive.navigationsuite"
 }
 
diff --git a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
index de80399..5e96cd5 100644
--- a/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive-navigation-suite/src/commonMain/kotlin/androidx/compose/material3/adaptive/navigationsuite/NavigationSuiteScaffold.kt
@@ -381,6 +381,16 @@
         val NavigationDrawer = NavigationSuiteType(description = "NavigationDrawer")
 
         /**
+         * A navigation suite type that instructs the [NavigationSuite] to perform specialized
+         * custom behavior. Different `NavigationSuite` implementations will exhibit different
+         * behaviors when using this type.
+         */
+        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+        @get:ExperimentalMaterial3AdaptiveNavigationSuiteApi
+        @ExperimentalMaterial3AdaptiveNavigationSuiteApi
+        val Custom = NavigationSuiteType(description = "Custom")
+
+        /**
          * A navigation suite type that instructs the [NavigationSuite] to not display any
          * navigation components on the screen.
          */
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 286e981..c488a59 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -90,6 +90,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material3.common"
 }
 
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index e415e38..f9109995 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -104,5 +104,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material3.windowsizeclass"
 }
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 333fdf4..9b09714 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -129,6 +129,7 @@
 
 // Screenshot tests related setup
 android {
+    compileSdk 35
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/compose/material3/material3"
     namespace "androidx.compose.material3"
diff --git a/compose/material3/material3/integration-tests/material3-demos/build.gradle b/compose/material3/material3/integration-tests/material3-demos/build.gradle
index 4902826..539b8cb 100644
--- a/compose/material3/material3/integration-tests/material3-demos/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-demos/build.gradle
@@ -55,5 +55,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.material3.demos"
 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index 624415c..61654d2 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -567,6 +567,7 @@
                     latch.countDown()
                 }
 
+                @Deprecated("deprecated")
                 override fun onLowMemory() {
                     // NO-OP
                 }
@@ -622,6 +623,7 @@
                     latch.countDown()
                 }
 
+                @Deprecated("deprecated")
                 override fun onLowMemory() {
                     // NO-OP
                 }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 5a06bfd..eadf4b8 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -209,6 +209,7 @@
                     latch.countDown()
                 }
 
+                @Deprecated("deprecated")
                 override fun onLowMemory() {
                     // NO-OP
                 }
@@ -265,6 +266,7 @@
                     latch.countDown()
                 }
 
+                @Deprecated("deprecated")
                 override fun onLowMemory() {
                     // NO-OP
                 }
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index 45c128e..98fdb63 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -114,5 +114,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.runtime.saveable"
 }
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index 588e594..5038a00 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -93,6 +93,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.runtime.integrationtests"
 }
 
@@ -177,4 +178,4 @@
         task.source = findFile()
         task.sizes = project.findProperty("compose.newExpectedSizes")
     }
-}
\ No newline at end of file
+}
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 7745525..1fe35c8 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -107,5 +107,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.testutils"
 }
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
index f96bd8a..680479b 100644
--- a/compose/ui/ui-graphics/benchmark/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -34,5 +34,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.graphics.benchmark"
 }
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index d761f6d..202ab22 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -123,5 +123,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.graphics"
 }
diff --git a/compose/ui/ui-graphics/samples/build.gradle b/compose/ui/ui-graphics/samples/build.gradle
index 2540cfd..927431d 100644
--- a/compose/ui/ui-graphics/samples/build.gradle
+++ b/compose/ui/ui-graphics/samples/build.gradle
@@ -50,5 +50,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.graphics.samples"
 }
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index 2de13ab..ed2e9d0 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -62,6 +62,7 @@
                         // NO-OP
                     }
 
+                    @Suppress("OVERRIDE_DEPRECATION")
                     override fun onLowMemory() {
                         // NO-OP
                     }
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
index 91da4b4..93f415a 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidImageBitmap.android.kt
@@ -66,7 +66,7 @@
         get() = bitmap.height
 
     override val config: ImageBitmapConfig
-        get() = bitmap.config.toImageConfig()
+        get() = bitmap.config!!.toImageConfig()
 
     override val colorSpace: ColorSpace
         get() =
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 33019de..fbd989f 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -214,7 +214,7 @@
     override fun getBounds(): Rect {
         if (rectF == null) rectF = PlatformRectF()
         with(rectF!!) {
-            internalPath.computeBounds(this, true)
+            @Suppress("DEPRECATION") internalPath.computeBounds(this, true)
             return Rect(this.left, this.top, this.right, this.bottom)
         }
     }
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
index 4c3e7ca..f0758b9 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/IntervalTree.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.graphics
 
 import androidx.annotation.RestrictTo
+import kotlin.collections.removeLast as removeLastKt
 import kotlin.math.max
 import kotlin.math.min
 
@@ -152,7 +153,11 @@
             val s = stack
             s.add(root)
             while (s.size > 0) {
-                val node = s.removeLast()
+
+                // MutableCollections.removeLast() is shadowed by java.util.list.removeAt()
+                // which was added in sdk 35 making this call unsafe
+                // val node = s.removeLast()
+                val node = s.removeLastKt()
                 if (node.overlaps(start, end)) block(node)
                 if (node.left !== terminator && node.left.max >= start) {
                     s.add(node.left)
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 20b1101..b7d7bbd 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -80,6 +80,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         // layout inspection supported starting on Android Q
         minSdkVersion 29
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
index b38bed3..e5a9a65 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -52,6 +52,7 @@
 import androidx.inspection.InspectorFactory
 import com.google.protobuf.ByteString
 import com.google.protobuf.InvalidProtocolBufferException
+import kotlin.collections.removeLast as removeLastKt
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersCommand
 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersResponse
@@ -97,7 +98,7 @@
             val stack = mutableListOf<InspectorNode>()
             trees.forEach { stack.addAll(it.nodes) }
             while (stack.isNotEmpty()) {
-                val node = stack.removeLast()
+                val node = stack.removeLastKt()
                 stack.addAll(node.children)
                 result.put(node.id, node)
             }
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 7bb5e9b..46b766d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -112,6 +112,7 @@
 
 
 android {
+    compileSdk 35
     lintOptions {
         disable("InvalidPackage")
     }
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
index 4fd6efb..387b078 100644
--- a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
@@ -33,5 +33,6 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "androidx.compose.ui.test.manifest.integration.testapp"
 }
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index b780393..e5c600c 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -135,6 +135,7 @@
 }
 
 android {
+    compileSdk 35
     sourceSets {
         test.java.srcDirs += "src/androidCommonTest/kotlin"
         androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
diff --git a/compose/ui/ui-test/samples/build.gradle b/compose/ui/ui-test/samples/build.gradle
index 2f926bc..7567c76 100644
--- a/compose/ui/ui-test/samples/build.gradle
+++ b/compose/ui/ui-test/samples/build.gradle
@@ -51,5 +51,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.test.samples"
 }
diff --git a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
index 5670ba3..de21d68 100644
--- a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
+++ b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/KeyAndMouseEventsTest.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.test.util.verifyMouseEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth
+import kotlin.collections.removeFirst as removeFirstKt
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -447,7 +448,7 @@
     }
 
     private fun <E> MutableList<E>.removeFirst(n: Int): List<E> {
-        return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirst()) } }
+        return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirstKt()) } }
     }
 
     private fun enqueueKeyPress(key: Key) {
diff --git a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
index c8bac18..829d278 100644
--- a/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
+++ b/compose/ui/ui-test/src/androidUnitTest/kotlin/androidx/compose/ui/test/inputdispatcher/MouseEventsTest.kt
@@ -45,6 +45,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.collections.removeFirst as removeFirstKt
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
@@ -815,6 +816,6 @@
     }
 
     private fun <E> MutableList<E>.removeFirst(n: Int): List<E> {
-        return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirst()) } }
+        return mutableListOf<E>().also { result -> repeat(n) { result.add(removeFirstKt()) } }
     }
 }
diff --git a/compose/ui/ui-text-google-fonts/build.gradle b/compose/ui/ui-text-google-fonts/build.gradle
index 06afb53..10747c9 100644
--- a/compose/ui/ui-text-google-fonts/build.gradle
+++ b/compose/ui/ui-text-google-fonts/build.gradle
@@ -56,5 +56,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.text.googlefonts"
 }
diff --git a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
index 9789e31..8859e04 100644
--- a/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
+++ b/compose/ui/ui-text-google-fonts/src/main/java/androidx/compose/ui/text/googlefonts/FontProviderHelper.kt
@@ -81,7 +81,7 @@
     @Suppress("DEPRECATION")
     @SuppressLint("PackageManagerGetSignatures")
     val packageInfo: PackageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
-    @Suppress("DEPRECATION") return convertToByteArrayList(packageInfo.signatures)
+    @Suppress("DEPRECATION") return convertToByteArrayList(packageInfo.signatures!!)
 }
 
 private val ByteArrayComparator = Comparator { l: ByteArray, r: ByteArray ->
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index bf6e10a..8bfa343 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -39,5 +39,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.text.benchmark"
 }
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 3799ef5..4dd85da 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -140,6 +140,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.text"
     // TODO(b/328001575)
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-text/samples/build.gradle b/compose/ui/ui-text/samples/build.gradle
index b6d1240..f19e8a0 100644
--- a/compose/ui/ui-text/samples/build.gradle
+++ b/compose/ui/ui-text/samples/build.gradle
@@ -51,6 +51,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.text.samples"
     // TODO(b/328001575)
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
index 9f676b8..617f75c 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/matchers/BitmapSubject.kt
@@ -56,7 +56,7 @@
 
     override fun actualCustomStringRepresentation(): String {
         return if (subject != null) {
-            "($subject ${subject.width}x${subject.height} ${subject.config.name})"
+            "($subject ${subject.width}x${subject.height} ${subject.config!!.name})"
         } else {
             super.actualCustomStringRepresentation()
         }
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index d7cd9ff..be053d0 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -109,5 +109,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.tooling.data"
 }
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index d72f2c9..c824c3e 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -118,6 +118,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.tooling"
     // TODO(b/345531033)
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
index 958368d..d373115 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.android.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.tooling.data.Group
 import androidx.compose.ui.tooling.data.UiToolingDataApi
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import kotlin.collections.removeLast as removeLastKt
 
 /** Tries to find the [Class] of the [PreviewParameterProvider] corresponding to the given FQN. */
 internal fun String.asPreviewProviderClass(): Class<out PreviewParameterProvider<*>>? {
@@ -119,7 +120,7 @@
     val result = mutableListOf<Group>()
     val stack = mutableListOf(root)
     while (stack.isNotEmpty()) {
-        val current = stack.removeLast()
+        val current = stack.removeLastKt()
         if (predicate(current)) {
             if (findOnlyFirst) {
                 return listOf(current)
diff --git a/compose/ui/ui-unit/samples/build.gradle b/compose/ui/ui-unit/samples/build.gradle
index ae1cb6e..42100f5 100644
--- a/compose/ui/ui-unit/samples/build.gradle
+++ b/compose/ui/ui-unit/samples/build.gradle
@@ -51,5 +51,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.unit.samples"
 }
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index 5e60265..9070854 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -59,5 +59,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.viewbinding"
 }
diff --git a/compose/ui/ui-viewbinding/samples/build.gradle b/compose/ui/ui-viewbinding/samples/build.gradle
index c2e2d2c..05dbaa8 100644
--- a/compose/ui/ui-viewbinding/samples/build.gradle
+++ b/compose/ui/ui-viewbinding/samples/build.gradle
@@ -58,6 +58,7 @@
 }
 
 android {
+    compileSdk 35
     buildFeatures {
         viewBinding true
     }
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 02f2ad8a..765fd5a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -191,6 +191,10 @@
   public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
   }
 
+  public final class SensitiveContentKt {
+    method public static androidx.compose.ui.Modifier sensitiveContent(androidx.compose.ui.Modifier, optional boolean isContentSensitive);
+  }
+
   @androidx.compose.runtime.ComposableTargetMarker(description="UI Composable") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.TYPE_PARAMETER}) public @interface UiComposable {
   }
 
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d7fbf23..b89aeb3 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -191,6 +191,10 @@
   public static final class MotionDurationScale.Key implements kotlin.coroutines.CoroutineContext.Key<androidx.compose.ui.MotionDurationScale> {
   }
 
+  public final class SensitiveContentKt {
+    method public static androidx.compose.ui.Modifier sensitiveContent(androidx.compose.ui.Modifier, optional boolean isContentSensitive);
+  }
+
   @androidx.compose.runtime.ComposableTargetMarker(description="UI Composable") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FILE, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.TYPE, kotlin.annotation.AnnotationTarget.TYPE_PARAMETER}) public @interface UiComposable {
   }
 
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 0d5ceba..ce4b1c2 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -205,6 +205,7 @@
 }
 
 android {
+    compileSdk 35
     testOptions.unitTests.includeAndroidResources = true
     buildTypes.configureEach {
         consumerProguardFiles("proguard-rules.pro")
diff --git a/compose/ui/ui/integration-tests/ui-demos/build.gradle b/compose/ui/ui/integration-tests/ui-demos/build.gradle
index 4c94cac..70d9088 100644
--- a/compose/ui/ui/integration-tests/ui-demos/build.gradle
+++ b/compose/ui/ui/integration-tests/ui-demos/build.gradle
@@ -43,6 +43,7 @@
 }
 
 android {
+    compileSdk 35
     buildFeatures {
         viewBinding true
     }
diff --git a/compose/ui/ui/samples/build.gradle b/compose/ui/ui/samples/build.gradle
index ffe2ad7..ed4179a 100644
--- a/compose/ui/ui/samples/build.gradle
+++ b/compose/ui/ui/samples/build.gradle
@@ -53,5 +53,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.compose.ui.samples"
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt
new file mode 100644
index 0000000..74c3f89
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/SensitiveContentModifierTest.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import android.os.Build
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.draw.Row
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(35)
+class SensitiveContentModifierTest {
+    @get:Rule val rule = createComposeRule()
+    private lateinit var androidComposeView: View
+
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+        Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun assertModifierEquals() {
+        val modifier = Modifier.sensitiveContent()
+        Assert.assertEquals(modifier, modifier)
+        Assert.assertEquals(modifier, Modifier.sensitiveContent())
+        Assert.assertNotEquals(modifier, Modifier.sensitiveContent(false))
+    }
+
+    @Test
+    fun testSensitiveContent() {
+        rule.setContent {
+            Text("User name", modifier = Modifier.sensitiveContent())
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testNonSensitiveContent() {
+        rule.setContent {
+            Text("Hello world!")
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testDynamicSensitiveModifier() {
+        var isContentSensitive by mutableStateOf(true)
+        rule.setContent {
+            Text(
+                "Dynamic sensitive Content",
+                modifier = Modifier.sensitiveContent(isContentSensitive)
+            )
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // update modifier as non sensitive and verify
+        rule.runOnIdle { isContentSensitive = false }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testMultipleSensitiveComposable() {
+        var isContentSensitive by mutableStateOf(true)
+        var isContentSensitive2 by mutableStateOf(true)
+        rule.setContent {
+            Row {
+                Text(
+                    "Dynamic sensitive Content",
+                    modifier = if (isContentSensitive) Modifier.sensitiveContent() else Modifier
+                )
+                Text(
+                    "Dynamic sensitive Content",
+                    modifier = if (isContentSensitive2) Modifier.sensitiveContent() else Modifier
+                )
+            }
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // mark one composable non sensitive and verify
+        rule.runOnIdle { isContentSensitive = false }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // mark another composable non sensitive and verify
+        rule.runOnIdle { isContentSensitive2 = false }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // mark one composable sensitive again and verify
+        rule.runOnIdle { isContentSensitive2 = true }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testRemoveSensitiveModifier() {
+        var isModifierIncluded by mutableStateOf(true)
+        rule.setContent {
+            Text(
+                "Dynamic sensitive Content",
+                modifier = if (isModifierIncluded) Modifier.sensitiveContent() else Modifier
+            )
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // remove sensitive modifier from composable and verify
+        rule.runOnIdle { isModifierIncluded = false }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testAddSensitiveModifier() {
+        var isModifierIncluded by mutableStateOf(false)
+        rule.setContent {
+            Text(
+                "Dynamic sensitive Content",
+                modifier = if (isModifierIncluded) Modifier.sensitiveContent() else Modifier
+            )
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // add sensitive modifier to the composable and verify
+        rule.runOnIdle { isModifierIncluded = true }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testRemoveSensitiveComposable() {
+        var isSensitiveComposableIncluded by mutableStateOf(true)
+        rule.setContent {
+            if (isSensitiveComposableIncluded) {
+                Text("Dynamic sensitive Content", modifier = Modifier.sensitiveContent())
+            }
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // remove sensitive composable and verify
+        rule.runOnIdle { isSensitiveComposableIncluded = false }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+
+    @Test
+    fun testAddSensitiveComposable() {
+        var isSensitiveComposableIncluded by mutableStateOf(false)
+        rule.setContent {
+            if (isSensitiveComposableIncluded) {
+                Text("Dynamic sensitive Content", modifier = Modifier.sensitiveContent())
+            }
+            androidComposeView = LocalView.current
+        }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_AUTO,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+
+        // add sensitive composable and verify
+        rule.runOnIdle { isSensitiveComposableIncluded = true }
+        rule.runOnIdle {
+            Assert.assertEquals(
+                View.CONTENT_SENSITIVITY_SENSITIVE,
+                androidComposeView.getContentSensitivity()
+            )
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 893f08a..1d0476c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -1152,6 +1152,7 @@
                     latch?.countDown()
                 }
 
+                @Deprecated("This callback is superseded by onTrimMemory")
                 override fun onLowMemory() {
                     // NO-OP
                 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
index b884769..d5169ed 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.android.kt
@@ -68,6 +68,7 @@
 /** Creates [PointerIcon] from pointer icon type (see [android.view.PointerIcon.getSystemIcon] */
 fun PointerIcon(pointerIconType: Int): PointerIcon = AndroidPointerIconType(pointerIconType)
 
+@Suppress("DEPRECATION")
 internal actual val pointerIconDefault: PointerIcon = AndroidPointerIconType(TYPE_DEFAULT)
 internal actual val pointerIconCrosshair: PointerIcon = AndroidPointerIconType(TYPE_CROSSHAIR)
 internal actual val pointerIconText: PointerIcon = AndroidPointerIconType(TYPE_TEXT)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 8e3e45e..da494eb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -2438,6 +2438,27 @@
 
     override fun shouldDelayChildPressedState(): Boolean = false
 
+    // Track sensitive composable visible in this view
+    private var sensitiveComponentCount = 0
+
+    override fun incrementSensitiveComponentCount() {
+        if (SDK_INT >= 35) {
+            if (sensitiveComponentCount == 0) {
+                AndroidComposeViewSensitiveContent35.setContentSensitivity(view, true)
+            }
+            sensitiveComponentCount += 1
+        }
+    }
+
+    override fun decrementSensitiveComponentCount() {
+        if (SDK_INT >= 35) {
+            if (sensitiveComponentCount == 1) {
+                AndroidComposeViewSensitiveContent35.setContentSensitivity(view, false)
+            }
+            sensitiveComponentCount -= 1
+        }
+    }
+
     companion object {
         private var systemPropertiesClass: Class<*>? = null
         private var getBooleanMethod: Method? = null
@@ -2621,6 +2642,19 @@
     fun calculateMatrixToWindow(view: View, matrix: Matrix)
 }
 
+@RequiresApi(35)
+private object AndroidComposeViewSensitiveContent35 {
+    @DoNotInline
+    @RequiresApi(35)
+    fun setContentSensitivity(view: View, isSensitiveContent: Boolean) {
+        if (isSensitiveContent) {
+            view.setContentSensitivity(View.CONTENT_SENSITIVITY_SENSITIVE)
+        } else {
+            view.setContentSensitivity(View.CONTENT_SENSITIVITY_AUTO)
+        }
+    }
+}
+
 @RequiresApi(Q)
 private class CalculateMatrixToWindowApi29 : CalculateMatrixToWindow {
     private val tmpMatrix = android.graphics.Matrix()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
index b865c80..fd0963c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt
@@ -126,6 +126,7 @@
                 resourceIdCache.clear()
             }
 
+            @Deprecated("This callback is superseded by onTrimMemory")
             override fun onLowMemory() {
                 resourceIdCache.clear()
             }
@@ -160,6 +161,7 @@
                 currentConfiguration.setTo(configuration)
             }
 
+            @Deprecated("This callback is superseded by onTrimMemory")
             override fun onLowMemory() {
                 imageVectorCache.clear()
             }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt
new file mode 100644
index 0000000..df250df
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/SensitiveContent.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import androidx.compose.ui.internal.checkPrecondition
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * This modifier hints that the composable renders sensitive content (i.e. username, password,
+ * credit card etc) on the screen, and the content should be protected during screen share in
+ * supported environments.
+ */
+fun Modifier.sensitiveContent(isContentSensitive: Boolean = true): Modifier =
+    this then SensitiveNodeElement(isContentSensitive)
+
+private data class SensitiveNodeElement(val isContentSensitive: Boolean) :
+    ModifierNodeElement<SensitiveContentNode>() {
+    override fun create(): SensitiveContentNode = SensitiveContentNode(isContentSensitive)
+
+    override fun update(node: SensitiveContentNode) {
+        node.isContentSensitive = isContentSensitive
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "sensitiveContent"
+        properties["isContentSensitive"] = isContentSensitive
+    }
+}
+
+private data class SensitiveContentNode(private var _isContentSensitive: Boolean) :
+    Modifier.Node() {
+    // Tracks if this node has been counted as sensitive or not.
+    private var isCountedSensitive: Boolean = false
+
+    var isContentSensitive: Boolean = _isContentSensitive
+        set(value) {
+            field = value
+            if (isContentSensitive && !isCountedSensitive) {
+                requireOwner().incrementSensitiveComponentCount()
+                isCountedSensitive = true
+            } else if (!isContentSensitive && isCountedSensitive) {
+                requireOwner().decrementSensitiveComponentCount()
+                isCountedSensitive = false
+            }
+        }
+
+    override fun onAttach() {
+        super.onAttach()
+        if (isContentSensitive) {
+            checkPrecondition(!isCountedSensitive) { "invalid sensitive content state" }
+            requireOwner().incrementSensitiveComponentCount()
+            isCountedSensitive = true
+        }
+    }
+
+    override fun onDetach() {
+        if (isCountedSensitive) {
+            requireOwner().decrementSensitiveComponentCount()
+            isCountedSensitive = false
+        }
+        super.onDetach()
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 1f93d1c..30d11e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -317,6 +317,20 @@
         session: suspend PlatformTextInputSessionScope.() -> Nothing
     ): Nothing
 
+    /**
+     * Tracks sensitive content on the screen to protect user privacy. Increment sensitive component
+     * count by 1. Implementation may protect user privacy by not showing sensitive content
+     * (username, password etc) to remote viewer during screen share.
+     */
+    fun incrementSensitiveComponentCount() {}
+
+    /**
+     * Tracks sensitive content on the screen to protect user privacy. Decrement sensitive component
+     * count by 1. Implementation may protect user privacy by not showing sensitive content
+     * (username, password etc) to remote viewer during screen share.
+     */
+    fun decrementSensitiveComponentCount() {}
+
     companion object {
         /**
          * Enables additional (and expensive to do in production) assertions. Useful to be set to
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 4412f9d..5b810b4 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -114,5 +114,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.constraintlayout.compose"
 }
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
index db7fe29..5afedb3 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/build.gradle
@@ -35,6 +35,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.constraintlayout.compose.demos"
 }
 
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
index 7b273f9..22a5142 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         minSdkVersion 26
     }
@@ -60,4 +61,4 @@
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-tooling"))
     implementation(project(":profileinstaller:profileinstaller"))
-}
\ No newline at end of file
+}
diff --git a/core/core-graphics-integration-tests/testapp/build.gradle b/core/core-graphics-integration-tests/testapp/build.gradle
index ea79373..10e75e1 100644
--- a/core/core-graphics-integration-tests/testapp/build.gradle
+++ b/core/core-graphics-integration-tests/testapp/build.gradle
@@ -21,6 +21,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.core.graphics.sample"
     }
diff --git a/core/core-ktx/build.gradle b/core/core-ktx/build.gradle
index f637c66..e71f40e 100644
--- a/core/core-ktx/build.gradle
+++ b/core/core-ktx/build.gradle
@@ -43,5 +43,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.core.ktx"
 }
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
index 0cd76e1..1aa472a 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
@@ -60,7 +60,7 @@
 
         val p = r1 + r2
         val r = RectF()
-        p.computeBounds(r, true)
+        @Suppress("DEPRECATION") p.computeBounds(r, true)
 
         assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
     }
@@ -72,7 +72,7 @@
 
         val p = r1 or r2
         val r = RectF()
-        p.computeBounds(r, true)
+        @Suppress("DEPRECATION") p.computeBounds(r, true)
 
         assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
     }
@@ -84,7 +84,7 @@
 
         val p = r1 - r2
         val r = RectF()
-        p.computeBounds(r, true)
+        @Suppress("DEPRECATION") p.computeBounds(r, true)
 
         assertEquals(RectF(0.0f, 0.0f, 5.0f, 10.0f), r)
     }
@@ -96,7 +96,7 @@
 
         val p = r1 and r2
         val r = RectF()
-        p.computeBounds(r, true)
+        @Suppress("DEPRECATION") p.computeBounds(r, true)
 
         assertEquals(RectF(5.0f, 0.0f, 10.0f, 10.0f), r)
     }
@@ -117,7 +117,7 @@
 
         val p = r1 xor r2
         val r = RectF()
-        p.computeBounds(r, true)
+        @Suppress("DEPRECATION") p.computeBounds(r, true)
 
         assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
     }
diff --git a/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt b/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
index 2577ae6..915b7a2 100644
--- a/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
+++ b/core/core-ktx/src/main/java/androidx/core/view/ViewGroup.kt
@@ -21,6 +21,7 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.annotation.Px
+import kotlin.collections.removeLast as removeLastKt
 
 /**
  * Returns the view at [index].
@@ -170,7 +171,10 @@
         } else {
             while (!iterator.hasNext() && stack.isNotEmpty()) {
                 iterator = stack.last()
-                stack.removeLast()
+                // MutableCollections.removeLast() is shadowed by java.util.list.removeAt()
+                // which was added in sdk 35 making this call unsafe
+                // stack.removeLast()
+                stack.removeLastKt()
             }
         }
     }
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 73c9942..4fc65ad 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -31,6 +31,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.core.splashscreen.samples"
     }
diff --git a/core/core-testing/build.gradle b/core/core-testing/build.gradle
index ce43285..053eb22 100644
--- a/core/core-testing/build.gradle
+++ b/core/core-testing/build.gradle
@@ -44,6 +44,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.core.testing"
 }
 
diff --git a/core/core/build.gradle b/core/core/build.gradle
index d4cac38..0b8acc6 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -71,6 +71,7 @@
 }
 
 android {
+    compileSdk = 35
     buildFeatures {
         aidl = true
     }
diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
index d213e1f..70bf03c 100644
--- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.os.Bundle;
 import android.os.Parcel;
 import android.support.v4.BaseInstrumentationTestCase;
 import android.text.SpannableStringBuilder;
@@ -91,6 +92,8 @@
     @Test
     public void testSetStylusHandwritingEnabled() {
         EditorInfo editorInfo = new EditorInfo();
+        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
         EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true /* enabled */);
         assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
 
@@ -99,6 +102,45 @@
     }
 
     @Test
+    public void testSetStylusHandwritingEnabled_compatWithCoreVersion1_13() {
+        EditorInfo editorInfo = new EditorInfo();
+        setStylusHandwritingEnabled_coreVersion1_13(editorInfo, false);
+        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+        editorInfo = new EditorInfo();
+        setStylusHandwritingEnabled_coreVersion1_13(editorInfo, true);
+        assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+        editorInfo = new EditorInfo();
+        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
+        assertFalse(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
+
+        editorInfo = new EditorInfo();
+        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
+        assertTrue(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 35)
+    public void testSetStylusHandwritingEnabled_compatWithAndroidV() {
+        EditorInfo editorInfo = new EditorInfo();
+        editorInfo.setStylusHandwritingEnabled(false);
+        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+        editorInfo = new EditorInfo();
+        editorInfo.setStylusHandwritingEnabled(true);
+        assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+
+        editorInfo = new EditorInfo();
+        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
+        assertFalse(editorInfo.isStylusHandwritingEnabled());
+
+        editorInfo = new EditorInfo();
+        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
+        assertTrue(editorInfo.isStylusHandwritingEnabled());
+    }
+
+    @Test
     public void setInitialText_nullInputText_throwsException() {
         final CharSequence testText = null;
         final EditorInfo editorInfo = new EditorInfo();
@@ -319,4 +361,22 @@
         }
         return builder;
     }
+
+    /** This is the version in AndroidX Core library 1.13. */
+    private static void setStylusHandwritingEnabled_coreVersion1_13(
+            EditorInfo editorInfo, boolean enabled) {
+        if (editorInfo.extras == null) {
+            editorInfo.extras = new Bundle();
+        }
+        editorInfo.extras.putBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY, enabled);
+    }
+
+    /** This is the version in AndroidX Core library 1.13. */
+    public static boolean isStylusHandwritingEnabled_coreVersion1_13(EditorInfo editorInfo) {
+        if (editorInfo.extras == null) {
+            // disabled by default
+            return false;
+        }
+        return editorInfo.extras.getBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY);
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
index 7ef503a..0686848 100644
--- a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
@@ -41,6 +41,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -104,7 +105,8 @@
     private static final String CONTENT_SELECTION_END_KEY =
             "androidx.core.view.inputmethod.EditorInfoCompat.CONTENT_SELECTION_END";
 
-    private static final String STYLUS_HANDWRITING_ENABLED_KEY =
+    @VisibleForTesting
+    static final String STYLUS_HANDWRITING_ENABLED_KEY =
             "androidx.core.view.inputmethod.EditorInfoCompat.STYLUS_HANDWRITING_ENABLED";
 
     @Retention(SOURCE)
@@ -209,6 +211,9 @@
      */
     public static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo,
             boolean enabled) {
+        if (BuildCompat.isAtLeastV()) {
+            Api35Impl.setStylusHandwritingEnabled(editorInfo, enabled);
+        }
         if (editorInfo.extras == null) {
             editorInfo.extras = new Bundle();
         }
@@ -222,11 +227,14 @@
      * @see InputMethodManager#isStylusHandwritingAvailable()
      */
     public static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
-        if (editorInfo.extras == null) {
-            // disabled by default
-            return false;
+        if (editorInfo.extras != null
+                && editorInfo.extras.containsKey(STYLUS_HANDWRITING_ENABLED_KEY)) {
+            return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
         }
-        return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
+        if (BuildCompat.isAtLeastV()) {
+            return Api35Impl.isStylusHandwritingEnabled(editorInfo);
+        }
+        return false;
     }
 
     /**
@@ -589,4 +597,17 @@
             return editorInfo.getInitialTextAfterCursor(length, flags);
         }
     }
+
+    @RequiresApi(35)
+    private static class Api35Impl {
+        private Api35Impl() {}
+
+        static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo, boolean enabled) {
+            editorInfo.setStylusHandwritingEnabled(enabled);
+        }
+
+        static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
+            return editorInfo.isStylusHandwritingEnabled();
+        }
+    }
 }
diff --git a/core/haptics/haptics/build.gradle b/core/haptics/haptics/build.gradle
index 9fcb353..073a92e6 100644
--- a/core/haptics/haptics/build.gradle
+++ b/core/haptics/haptics/build.gradle
@@ -43,6 +43,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.core.haptics"
 }
 
diff --git a/core/haptics/haptics/integration-tests/demos/build.gradle b/core/haptics/haptics/integration-tests/demos/build.gradle
index 9695d73..d4f87ae 100644
--- a/core/haptics/haptics/integration-tests/demos/build.gradle
+++ b/core/haptics/haptics/integration-tests/demos/build.gradle
@@ -28,6 +28,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.core.haptics.demos"
     }
diff --git a/core/haptics/haptics/samples/build.gradle b/core/haptics/haptics/samples/build.gradle
index 2f6769e..3892ef1 100644
--- a/core/haptics/haptics/samples/build.gradle
+++ b/core/haptics/haptics/samples/build.gradle
@@ -25,6 +25,7 @@
     implementation(project(":core:haptics:haptics"))
 }
 android {
+    compileSdk 35
     namespace "androidx.core.haptics.samples"
 }
 androidx {
diff --git a/credentials/credentials-fido/build.gradle b/credentials/credentials-fido/build.gradle
index 8d0fd93..89b1cd52e 100644
--- a/credentials/credentials-fido/build.gradle
+++ b/credentials/credentials-fido/build.gradle
@@ -46,6 +46,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.credentials.fido"
 }
 
diff --git a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials-play-services-auth/api/res-1.3.0-beta02.txt
+++ /dev/null
diff --git a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt b/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt
deleted file mode 100644
index e6f50d0..0000000
--- a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta02.txt
+++ /dev/null
@@ -1 +0,0 @@
-// Signature format: 4.0
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 6b0091c..72eaf1a 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -63,6 +63,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.credentials.play.services.auth"
 
     buildTypes.configureEach {
diff --git a/credentials/credentials/api/1.3.0-beta01.txt b/credentials/credentials/api/1.3.0-beta01.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/1.3.0-beta01.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
-  public final class ClearCredentialStateRequest {
-    ctor public ClearCredentialStateRequest();
-  }
-
-  public abstract class CreateCredentialRequest {
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getCredentialData();
-    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
-    method public final String? getOrigin();
-    method public final String getType();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    method public final boolean preferImmediatelyAvailableCredentials();
-    property public final android.os.Bundle candidateQueryData;
-    property public final android.os.Bundle credentialData;
-    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final String? origin;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final String type;
-    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo {
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
-    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-    method public CharSequence? getUserDisplayName();
-    method public CharSequence getUserId();
-    property public final CharSequence? userDisplayName;
-    property public final CharSequence userId;
-    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-  }
-
-  public abstract class CreateCredentialResponse {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-  }
-
-  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
-  }
-
-  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePasswordRequest(String id, String password);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
-    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-  }
-
-  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePasswordResponse();
-  }
-
-  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
-    method public String getRegistrationResponseJson();
-    property public final String registrationResponseJson;
-  }
-
-  public abstract class Credential {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public interface CredentialManager {
-    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
-    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
-    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    field public static final androidx.credentials.CredentialManager.Companion Companion;
-  }
-
-  public static final class CredentialManager.Companion {
-    method public androidx.credentials.CredentialManager create(android.content.Context context);
-  }
-
-  public interface CredentialManagerCallback<R, E> {
-    method public void onError(E e);
-    method public void onResult(R result);
-  }
-
-  public abstract class CredentialOption {
-    method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getRequestData();
-    method public final String getType();
-    method public final int getTypePriorityHint();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    property public final java.util.Set<android.content.ComponentName> allowedProviders;
-    property public final android.os.Bundle candidateQueryData;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final android.os.Bundle requestData;
-    property public final String type;
-    property public final int typePriorityHint;
-    field public static final androidx.credentials.CredentialOption.Companion Companion;
-    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
-    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
-    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
-    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
-  }
-
-  public static final class CredentialOption.Companion {
-  }
-
-  public interface CredentialProvider {
-    method public boolean isAvailableOnDevice();
-    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-  }
-
-  public class CustomCredential extends androidx.credentials.Credential {
-    ctor public CustomCredential(String type, android.os.Bundle data);
-  }
-
-  public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    method public String? getOrigin();
-    method public boolean getPreferIdentityDocUi();
-    method public android.content.ComponentName? getPreferUiBrandingComponentName();
-    method public boolean preferImmediatelyAvailableCredentials();
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-    property public final String? origin;
-    property public final boolean preferIdentityDocUi;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final android.content.ComponentName? preferUiBrandingComponentName;
-  }
-
-  public static final class GetCredentialRequest.Builder {
-    ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
-    method public androidx.credentials.GetCredentialRequest build();
-    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
-  }
-
-  public final class GetCredentialResponse {
-    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
-    method public androidx.credentials.Credential getCredential();
-    property public final androidx.credentials.Credential credential;
-  }
-
-  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
-  }
-
-  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
-    ctor public GetPasswordOption();
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class PasswordCredential extends androidx.credentials.Credential {
-    ctor public PasswordCredential(String id, String password);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-    field public static final androidx.credentials.PasswordCredential.Companion Companion;
-    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
-  }
-
-  public static final class PasswordCredential.Companion {
-  }
-
-  @RequiresApi(34) public final class PrepareGetCredentialResponse {
-    method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
-    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
-    method public boolean isNullHandlesForTest();
-    property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
-    property public final boolean isNullHandlesForTest;
-    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
-  }
-
-  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
-    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
-  }
-
-  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
-    ctor public PrepareGetCredentialResponse.TestBuilder();
-    method public androidx.credentials.PrepareGetCredentialResponse build();
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-  }
-
-  public final class PublicKeyCredential extends androidx.credentials.Credential {
-    ctor public PublicKeyCredential(String authenticationResponseJson);
-    method public String getAuthenticationResponseJson();
-    property public final String authenticationResponseJson;
-    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
-    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
-  }
-
-  public static final class PublicKeyCredential.Companion {
-  }
-
-}
-
-package androidx.credentials.exceptions {
-
-  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialCustomException(String type);
-    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class ClearCredentialException extends java.lang.Exception {
-  }
-
-  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialInterruptedException();
-    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialProviderConfigurationException();
-    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnknownException();
-    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnsupportedException();
-    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCancellationException();
-    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCustomException(String type);
-    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class CreateCredentialException extends java.lang.Exception {
-  }
-
-  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialInterruptedException();
-    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialNoCreateOptionException();
-    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialProviderConfigurationException();
-    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnknownException();
-    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnsupportedException();
-    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCancellationException();
-    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCustomException(String type);
-    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class GetCredentialException extends java.lang.Exception {
-  }
-
-  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialInterruptedException();
-    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialProviderConfigurationException();
-    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnknownException();
-    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnsupportedException();
-    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public NoCredentialException();
-    ctor public NoCredentialException(optional CharSequence? errorMessage);
-  }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
-  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public AbortError();
-  }
-
-  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ConstraintError();
-  }
-
-  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataCloneError();
-  }
-
-  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataError();
-  }
-
-  public abstract class DomError {
-    ctor public DomError(String type);
-  }
-
-  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public EncodingError();
-  }
-
-  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public HierarchyRequestError();
-  }
-
-  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InUseAttributeError();
-  }
-
-  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidCharacterError();
-  }
-
-  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidModificationError();
-  }
-
-  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidNodeTypeError();
-  }
-
-  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidStateError();
-  }
-
-  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NamespaceError();
-  }
-
-  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NetworkError();
-  }
-
-  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NoModificationAllowedError();
-  }
-
-  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotAllowedError();
-  }
-
-  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotFoundError();
-  }
-
-  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotReadableError();
-  }
-
-  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotSupportedError();
-  }
-
-  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OperationError();
-  }
-
-  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OptOutError();
-  }
-
-  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public QuotaExceededError();
-  }
-
-  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ReadOnlyError();
-  }
-
-  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SecurityError();
-  }
-
-  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SyntaxError();
-  }
-
-  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TimeoutError();
-  }
-
-  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TransactionInactiveError();
-  }
-
-  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public UnknownError();
-  }
-
-  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public VersionError();
-  }
-
-  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public WrongDocumentError();
-  }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
-  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
-  }
-
-  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-  }
-
-}
-
-package androidx.credentials.provider {
-
-  public final class Action {
-    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
-    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.Action.Companion Companion;
-  }
-
-  public static final class Action.Builder {
-    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.Action build();
-    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
-  }
-
-  public static final class Action.Companion {
-    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-  }
-
-  public final class AuthenticationAction {
-    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
-    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
-  }
-
-  public static final class AuthenticationAction.Builder {
-    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.AuthenticationAction build();
-  }
-
-  public static final class AuthenticationAction.Companion {
-    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-  }
-
-  public abstract class BeginCreateCredentialRequest {
-    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getType();
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    property public final android.os.Bundle candidateQueryData;
-    property public final String type;
-    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginCreateCredentialResponse {
-    ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialResponse.Builder {
-    ctor public BeginCreateCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginCreateCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-  }
-
-  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public abstract class BeginGetCredentialOption {
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getId();
-    method public final String getType();
-    property public final android.os.Bundle candidateQueryData;
-    property public final String id;
-    property public final String type;
-  }
-
-  public final class BeginGetCredentialRequest {
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
-    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginGetCredentialResponse {
-    ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.Action> getActions();
-    method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
-    method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.Action> actions;
-    property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
-    property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialResponse.Builder {
-    ctor public BeginGetCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
-    method public androidx.credentials.provider.BeginGetCredentialResponse build();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginGetCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CallingAppInfo {
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
-    method public String? getOrigin(String privilegedAllowlist);
-    method public String getPackageName();
-    method public android.content.pm.SigningInfo getSigningInfo();
-    method public boolean isOriginPopulated();
-    property public final String packageName;
-    property public final android.content.pm.SigningInfo signingInfo;
-  }
-
-  @RequiresApi(26) public final class CreateEntry {
-    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
-    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-    method public CharSequence getAccountName();
-    method public CharSequence? getDescription();
-    method public android.graphics.drawable.Icon? getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public Integer? getPasswordCredentialCount();
-    method public android.app.PendingIntent getPendingIntent();
-    method public Integer? getPublicKeyCredentialCount();
-    method public Integer? getTotalCredentialCount();
-    method public boolean isAutoSelectAllowed();
-    property public final CharSequence accountName;
-    property public final CharSequence? description;
-    property public final android.graphics.drawable.Icon? icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
-  }
-
-  public static final class CreateEntry.Builder {
-    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.CreateEntry build();
-    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
-    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
-    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
-  }
-
-  public static final class CreateEntry.Companion {
-    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-  }
-
-  public abstract class CredentialEntry {
-    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public final CharSequence? getAffiliatedDomain();
-    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
-    method public final CharSequence getEntryGroupId();
-    method public final boolean isDefaultIconPreferredAsSingleProvider();
-    property public final CharSequence? affiliatedDomain;
-    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
-    property public final CharSequence entryGroupId;
-    property public final boolean isDefaultIconPreferredAsSingleProvider;
-    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
-  }
-
-  public static final class CredentialEntry.Companion {
-    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
-    ctor public CredentialProviderService();
-    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
-    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
-    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
-    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-  }
-
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    method public String getType();
-    method public CharSequence? getTypeDisplayName();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    property public String type;
-    property public final CharSequence? typeDisplayName;
-    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
-  }
-
-  public static final class CustomCredentialEntry.Builder {
-    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
-    method public androidx.credentials.provider.CustomCredentialEntry build();
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
-  }
-
-  public static final class CustomCredentialEntry.Companion {
-    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class IntentHandlerConverters {
-    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
-  }
-
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
-  }
-
-  public static final class PasswordCredentialEntry.Builder {
-    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
-    method public androidx.credentials.provider.PasswordCredentialEntry build();
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PasswordCredentialEntry.Companion {
-    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public final class PendingIntentHandler {
-    ctor public PendingIntentHandler();
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
-  }
-
-  public static final class PendingIntentHandler.Companion {
-    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-  }
-
-  public final class ProviderClearCredentialStateRequest {
-    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-  }
-
-  public final class ProviderCreateCredentialRequest {
-    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final androidx.credentials.CreateCredentialRequest callingRequest;
-  }
-
-  public final class ProviderGetCredentialRequest {
-    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-  }
-
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
-  }
-
-  public static final class PublicKeyCredentialEntry.Builder {
-    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PublicKeyCredentialEntry.Companion {
-    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class RemoteEntry {
-    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
-    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-    method public android.app.PendingIntent getPendingIntent();
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
-  }
-
-  public static final class RemoteEntry.Builder {
-    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.RemoteEntry build();
-  }
-
-  public static final class RemoteEntry.Companion {
-    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-  }
-
-}
-
diff --git a/credentials/credentials/api/1.3.0-beta02.txt b/credentials/credentials/api/1.3.0-beta02.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/1.3.0-beta02.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
-  public final class ClearCredentialStateRequest {
-    ctor public ClearCredentialStateRequest();
-  }
-
-  public abstract class CreateCredentialRequest {
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getCredentialData();
-    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
-    method public final String? getOrigin();
-    method public final String getType();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    method public final boolean preferImmediatelyAvailableCredentials();
-    property public final android.os.Bundle candidateQueryData;
-    property public final android.os.Bundle credentialData;
-    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final String? origin;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final String type;
-    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo {
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
-    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-    method public CharSequence? getUserDisplayName();
-    method public CharSequence getUserId();
-    property public final CharSequence? userDisplayName;
-    property public final CharSequence userId;
-    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-  }
-
-  public abstract class CreateCredentialResponse {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-  }
-
-  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
-  }
-
-  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePasswordRequest(String id, String password);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
-    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-  }
-
-  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePasswordResponse();
-  }
-
-  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
-    method public String getRegistrationResponseJson();
-    property public final String registrationResponseJson;
-  }
-
-  public abstract class Credential {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public interface CredentialManager {
-    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
-    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
-    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    field public static final androidx.credentials.CredentialManager.Companion Companion;
-  }
-
-  public static final class CredentialManager.Companion {
-    method public androidx.credentials.CredentialManager create(android.content.Context context);
-  }
-
-  public interface CredentialManagerCallback<R, E> {
-    method public void onError(E e);
-    method public void onResult(R result);
-  }
-
-  public abstract class CredentialOption {
-    method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getRequestData();
-    method public final String getType();
-    method public final int getTypePriorityHint();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    property public final java.util.Set<android.content.ComponentName> allowedProviders;
-    property public final android.os.Bundle candidateQueryData;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final android.os.Bundle requestData;
-    property public final String type;
-    property public final int typePriorityHint;
-    field public static final androidx.credentials.CredentialOption.Companion Companion;
-    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
-    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
-    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
-    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
-  }
-
-  public static final class CredentialOption.Companion {
-  }
-
-  public interface CredentialProvider {
-    method public boolean isAvailableOnDevice();
-    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-  }
-
-  public class CustomCredential extends androidx.credentials.Credential {
-    ctor public CustomCredential(String type, android.os.Bundle data);
-  }
-
-  public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    method public String? getOrigin();
-    method public boolean getPreferIdentityDocUi();
-    method public android.content.ComponentName? getPreferUiBrandingComponentName();
-    method public boolean preferImmediatelyAvailableCredentials();
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-    property public final String? origin;
-    property public final boolean preferIdentityDocUi;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final android.content.ComponentName? preferUiBrandingComponentName;
-  }
-
-  public static final class GetCredentialRequest.Builder {
-    ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
-    method public androidx.credentials.GetCredentialRequest build();
-    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
-  }
-
-  public final class GetCredentialResponse {
-    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
-    method public androidx.credentials.Credential getCredential();
-    property public final androidx.credentials.Credential credential;
-  }
-
-  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
-  }
-
-  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
-    ctor public GetPasswordOption();
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class PasswordCredential extends androidx.credentials.Credential {
-    ctor public PasswordCredential(String id, String password);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-    field public static final androidx.credentials.PasswordCredential.Companion Companion;
-    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
-  }
-
-  public static final class PasswordCredential.Companion {
-  }
-
-  @RequiresApi(34) public final class PrepareGetCredentialResponse {
-    method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
-    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
-    method public boolean isNullHandlesForTest();
-    property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
-    property public final boolean isNullHandlesForTest;
-    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
-  }
-
-  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
-    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
-  }
-
-  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
-    ctor public PrepareGetCredentialResponse.TestBuilder();
-    method public androidx.credentials.PrepareGetCredentialResponse build();
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-  }
-
-  public final class PublicKeyCredential extends androidx.credentials.Credential {
-    ctor public PublicKeyCredential(String authenticationResponseJson);
-    method public String getAuthenticationResponseJson();
-    property public final String authenticationResponseJson;
-    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
-    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
-  }
-
-  public static final class PublicKeyCredential.Companion {
-  }
-
-}
-
-package androidx.credentials.exceptions {
-
-  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialCustomException(String type);
-    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class ClearCredentialException extends java.lang.Exception {
-  }
-
-  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialInterruptedException();
-    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialProviderConfigurationException();
-    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnknownException();
-    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnsupportedException();
-    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCancellationException();
-    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCustomException(String type);
-    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class CreateCredentialException extends java.lang.Exception {
-  }
-
-  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialInterruptedException();
-    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialNoCreateOptionException();
-    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialProviderConfigurationException();
-    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnknownException();
-    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnsupportedException();
-    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCancellationException();
-    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCustomException(String type);
-    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class GetCredentialException extends java.lang.Exception {
-  }
-
-  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialInterruptedException();
-    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialProviderConfigurationException();
-    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnknownException();
-    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnsupportedException();
-    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public NoCredentialException();
-    ctor public NoCredentialException(optional CharSequence? errorMessage);
-  }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
-  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public AbortError();
-  }
-
-  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ConstraintError();
-  }
-
-  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataCloneError();
-  }
-
-  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataError();
-  }
-
-  public abstract class DomError {
-    ctor public DomError(String type);
-  }
-
-  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public EncodingError();
-  }
-
-  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public HierarchyRequestError();
-  }
-
-  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InUseAttributeError();
-  }
-
-  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidCharacterError();
-  }
-
-  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidModificationError();
-  }
-
-  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidNodeTypeError();
-  }
-
-  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidStateError();
-  }
-
-  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NamespaceError();
-  }
-
-  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NetworkError();
-  }
-
-  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NoModificationAllowedError();
-  }
-
-  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotAllowedError();
-  }
-
-  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotFoundError();
-  }
-
-  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotReadableError();
-  }
-
-  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotSupportedError();
-  }
-
-  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OperationError();
-  }
-
-  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OptOutError();
-  }
-
-  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public QuotaExceededError();
-  }
-
-  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ReadOnlyError();
-  }
-
-  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SecurityError();
-  }
-
-  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SyntaxError();
-  }
-
-  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TimeoutError();
-  }
-
-  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TransactionInactiveError();
-  }
-
-  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public UnknownError();
-  }
-
-  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public VersionError();
-  }
-
-  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public WrongDocumentError();
-  }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
-  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
-  }
-
-  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-  }
-
-}
-
-package androidx.credentials.provider {
-
-  public final class Action {
-    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
-    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.Action.Companion Companion;
-  }
-
-  public static final class Action.Builder {
-    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.Action build();
-    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
-  }
-
-  public static final class Action.Companion {
-    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-  }
-
-  public final class AuthenticationAction {
-    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
-    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
-  }
-
-  public static final class AuthenticationAction.Builder {
-    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.AuthenticationAction build();
-  }
-
-  public static final class AuthenticationAction.Companion {
-    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-  }
-
-  public abstract class BeginCreateCredentialRequest {
-    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getType();
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    property public final android.os.Bundle candidateQueryData;
-    property public final String type;
-    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginCreateCredentialResponse {
-    ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialResponse.Builder {
-    ctor public BeginCreateCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginCreateCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-  }
-
-  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public abstract class BeginGetCredentialOption {
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getId();
-    method public final String getType();
-    property public final android.os.Bundle candidateQueryData;
-    property public final String id;
-    property public final String type;
-  }
-
-  public final class BeginGetCredentialRequest {
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
-    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginGetCredentialResponse {
-    ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.Action> getActions();
-    method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
-    method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.Action> actions;
-    property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
-    property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialResponse.Builder {
-    ctor public BeginGetCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
-    method public androidx.credentials.provider.BeginGetCredentialResponse build();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginGetCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CallingAppInfo {
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
-    method public String? getOrigin(String privilegedAllowlist);
-    method public String getPackageName();
-    method public android.content.pm.SigningInfo getSigningInfo();
-    method public boolean isOriginPopulated();
-    property public final String packageName;
-    property public final android.content.pm.SigningInfo signingInfo;
-  }
-
-  @RequiresApi(26) public final class CreateEntry {
-    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
-    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-    method public CharSequence getAccountName();
-    method public CharSequence? getDescription();
-    method public android.graphics.drawable.Icon? getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public Integer? getPasswordCredentialCount();
-    method public android.app.PendingIntent getPendingIntent();
-    method public Integer? getPublicKeyCredentialCount();
-    method public Integer? getTotalCredentialCount();
-    method public boolean isAutoSelectAllowed();
-    property public final CharSequence accountName;
-    property public final CharSequence? description;
-    property public final android.graphics.drawable.Icon? icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
-  }
-
-  public static final class CreateEntry.Builder {
-    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.CreateEntry build();
-    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
-    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
-    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
-  }
-
-  public static final class CreateEntry.Companion {
-    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-  }
-
-  public abstract class CredentialEntry {
-    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public final CharSequence? getAffiliatedDomain();
-    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
-    method public final CharSequence getEntryGroupId();
-    method public final boolean isDefaultIconPreferredAsSingleProvider();
-    property public final CharSequence? affiliatedDomain;
-    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
-    property public final CharSequence entryGroupId;
-    property public final boolean isDefaultIconPreferredAsSingleProvider;
-    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
-  }
-
-  public static final class CredentialEntry.Companion {
-    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
-    ctor public CredentialProviderService();
-    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
-    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
-    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
-    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-  }
-
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    method public String getType();
-    method public CharSequence? getTypeDisplayName();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    property public String type;
-    property public final CharSequence? typeDisplayName;
-    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
-  }
-
-  public static final class CustomCredentialEntry.Builder {
-    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
-    method public androidx.credentials.provider.CustomCredentialEntry build();
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
-  }
-
-  public static final class CustomCredentialEntry.Companion {
-    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class IntentHandlerConverters {
-    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
-  }
-
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
-  }
-
-  public static final class PasswordCredentialEntry.Builder {
-    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
-    method public androidx.credentials.provider.PasswordCredentialEntry build();
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PasswordCredentialEntry.Companion {
-    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public final class PendingIntentHandler {
-    ctor public PendingIntentHandler();
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
-  }
-
-  public static final class PendingIntentHandler.Companion {
-    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-  }
-
-  public final class ProviderClearCredentialStateRequest {
-    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-  }
-
-  public final class ProviderCreateCredentialRequest {
-    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final androidx.credentials.CreateCredentialRequest callingRequest;
-  }
-
-  public final class ProviderGetCredentialRequest {
-    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-  }
-
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
-  }
-
-  public static final class PublicKeyCredentialEntry.Builder {
-    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PublicKeyCredentialEntry.Companion {
-    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class RemoteEntry {
-    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
-    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-    method public android.app.PendingIntent getPendingIntent();
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
-  }
-
-  public static final class RemoteEntry.Builder {
-    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.RemoteEntry build();
-  }
-
-  public static final class RemoteEntry.Companion {
-    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-  }
-
-}
-
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index a0d0eda..8b50742 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -133,6 +133,11 @@
     method public void onResult(R result);
   }
 
+  public final class CredentialManagerViewHandler {
+    method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
+    method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
+  }
+
   public abstract class CredentialOption {
     method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
     method public final android.os.Bundle getCandidateQueryData();
@@ -245,6 +250,14 @@
   public static final class PasswordCredential.Companion {
   }
 
+  public final class PendingGetCredentialRequest {
+    ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1<? super androidx.credentials.GetCredentialResponse,kotlin.Unit> callback);
+    method public kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> getCallback();
+    method public androidx.credentials.GetCredentialRequest getRequest();
+    property public final kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> callback;
+    property public final androidx.credentials.GetCredentialRequest request;
+  }
+
   @RequiresApi(34) public final class PrepareGetCredentialResponse {
     method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
     method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
@@ -591,6 +604,21 @@
     method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
   }
 
+  public final class AuthenticationError {
+    ctor public AuthenticationError(int errorCode);
+    ctor public AuthenticationError(int errorCode, optional CharSequence? errorMsg);
+    method public int getErrorCode();
+    method public CharSequence? getErrorMsg();
+    property public final int errorCode;
+    property public final CharSequence? errorMsg;
+  }
+
+  public final class AuthenticationResult {
+    ctor public AuthenticationResult(int authenticationType);
+    method public int getAuthenticationType();
+    property public final int authenticationType;
+  }
+
   public abstract class BeginCreateCredentialRequest {
     ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
     method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
@@ -729,6 +757,33 @@
     property public final String requestJson;
   }
 
+  @RequiresApi(35) public final class BiometricPromptData {
+    ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
+    ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
+    method public int getAllowedAuthenticators();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+    property public final int allowedAuthenticators;
+    property public final androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject;
+  }
+
+  public static final class BiometricPromptData.Builder {
+    ctor public BiometricPromptData.Builder();
+    method public androidx.credentials.provider.BiometricPromptData build();
+    method public androidx.credentials.provider.BiometricPromptData.Builder setAllowedAuthenticators(int allowedAuthenticators);
+    method public androidx.credentials.provider.BiometricPromptData.Builder setCryptoObject(androidx.biometric.BiometricPrompt.CryptoObject cryptoObject);
+  }
+
+  public final class BiometricPromptResult {
+    ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationError authenticationError);
+    ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationResult authenticationResult);
+    method public androidx.credentials.provider.AuthenticationError? getAuthenticationError();
+    method public androidx.credentials.provider.AuthenticationResult? getAuthenticationResult();
+    method public boolean isSuccessful();
+    property public final androidx.credentials.provider.AuthenticationError? authenticationError;
+    property public final androidx.credentials.provider.AuthenticationResult? authenticationResult;
+    property public final boolean isSuccessful;
+  }
+
   public final class CallingAppInfo {
     ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
     ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
@@ -740,10 +795,12 @@
     property public final android.content.pm.SigningInfo signingInfo;
   }
 
-  @RequiresApi(26) public final class CreateEntry {
+  @RequiresApi(23) public final class CreateEntry {
     ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
     method public CharSequence getAccountName();
+    method public androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
     method public CharSequence? getDescription();
     method public android.graphics.drawable.Icon? getIcon();
     method public java.time.Instant? getLastUsedTime();
@@ -753,6 +810,7 @@
     method public Integer? getTotalCredentialCount();
     method public boolean isAutoSelectAllowed();
     property public final CharSequence accountName;
+    property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
     property public final CharSequence? description;
     property public final android.graphics.drawable.Icon? icon;
     property public final boolean isAutoSelectAllowed;
@@ -765,6 +823,7 @@
     ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
     method public androidx.credentials.provider.CreateEntry build();
     method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CreateEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
     method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
     method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
@@ -781,10 +840,12 @@
     method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public final CharSequence? getAffiliatedDomain();
     method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+    method public final androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
     method public final CharSequence getEntryGroupId();
     method public final boolean isDefaultIconPreferredAsSingleProvider();
     property public final CharSequence? affiliatedDomain;
     property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+    property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
     property public final CharSequence entryGroupId;
     property public final boolean isDefaultIconPreferredAsSingleProvider;
     field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
@@ -804,9 +865,10 @@
     method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
   }
 
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public android.graphics.drawable.Icon getIcon();
     method public java.time.Instant? getLastUsedTime();
@@ -835,6 +897,7 @@
     ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
     method public androidx.credentials.provider.CustomCredentialEntry build();
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CustomCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -855,9 +918,10 @@
     method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
   }
 
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public CharSequence? getDisplayName();
     method public android.graphics.drawable.Icon getIcon();
@@ -885,6 +949,7 @@
     method public androidx.credentials.provider.PasswordCredentialEntry build();
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PasswordCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -927,23 +992,30 @@
 
   public final class ProviderCreateCredentialRequest {
     ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+    method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
     method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
     method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+    property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
     property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
     property public final androidx.credentials.CreateCredentialRequest callingRequest;
   }
 
   public final class ProviderGetCredentialRequest {
     ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+    method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
     method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
     method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+    property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
     property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
     property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
   }
 
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public CharSequence? getDisplayName();
     method public android.graphics.drawable.Icon getIcon();
@@ -970,6 +1042,7 @@
     ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
     method public androidx.credentials.provider.PublicKeyCredentialEntry build();
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
diff --git a/credentials/credentials/api/res-1.3.0-beta01.txt b/credentials/credentials/api/res-1.3.0-beta01.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials/api/res-1.3.0-beta01.txt
+++ /dev/null
diff --git a/credentials/credentials/api/res-1.3.0-beta02.txt b/credentials/credentials/api/res-1.3.0-beta02.txt
deleted file mode 100644
index e69de29..0000000
--- a/credentials/credentials/api/res-1.3.0-beta02.txt
+++ /dev/null
diff --git a/credentials/credentials/api/restricted_1.3.0-beta01.txt b/credentials/credentials/api/restricted_1.3.0-beta01.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/restricted_1.3.0-beta01.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
-  public final class ClearCredentialStateRequest {
-    ctor public ClearCredentialStateRequest();
-  }
-
-  public abstract class CreateCredentialRequest {
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getCredentialData();
-    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
-    method public final String? getOrigin();
-    method public final String getType();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    method public final boolean preferImmediatelyAvailableCredentials();
-    property public final android.os.Bundle candidateQueryData;
-    property public final android.os.Bundle credentialData;
-    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final String? origin;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final String type;
-    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo {
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
-    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-    method public CharSequence? getUserDisplayName();
-    method public CharSequence getUserId();
-    property public final CharSequence? userDisplayName;
-    property public final CharSequence userId;
-    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-  }
-
-  public abstract class CreateCredentialResponse {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-  }
-
-  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
-  }
-
-  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePasswordRequest(String id, String password);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
-    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-  }
-
-  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePasswordResponse();
-  }
-
-  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
-    method public String getRegistrationResponseJson();
-    property public final String registrationResponseJson;
-  }
-
-  public abstract class Credential {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public interface CredentialManager {
-    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
-    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
-    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    field public static final androidx.credentials.CredentialManager.Companion Companion;
-  }
-
-  public static final class CredentialManager.Companion {
-    method public androidx.credentials.CredentialManager create(android.content.Context context);
-  }
-
-  public interface CredentialManagerCallback<R, E> {
-    method public void onError(E e);
-    method public void onResult(R result);
-  }
-
-  public abstract class CredentialOption {
-    method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getRequestData();
-    method public final String getType();
-    method public final int getTypePriorityHint();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    property public final java.util.Set<android.content.ComponentName> allowedProviders;
-    property public final android.os.Bundle candidateQueryData;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final android.os.Bundle requestData;
-    property public final String type;
-    property public final int typePriorityHint;
-    field public static final androidx.credentials.CredentialOption.Companion Companion;
-    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
-    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
-    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
-    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
-  }
-
-  public static final class CredentialOption.Companion {
-  }
-
-  public interface CredentialProvider {
-    method public boolean isAvailableOnDevice();
-    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-  }
-
-  public class CustomCredential extends androidx.credentials.Credential {
-    ctor public CustomCredential(String type, android.os.Bundle data);
-  }
-
-  public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    method public String? getOrigin();
-    method public boolean getPreferIdentityDocUi();
-    method public android.content.ComponentName? getPreferUiBrandingComponentName();
-    method public boolean preferImmediatelyAvailableCredentials();
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-    property public final String? origin;
-    property public final boolean preferIdentityDocUi;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final android.content.ComponentName? preferUiBrandingComponentName;
-  }
-
-  public static final class GetCredentialRequest.Builder {
-    ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
-    method public androidx.credentials.GetCredentialRequest build();
-    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
-  }
-
-  public final class GetCredentialResponse {
-    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
-    method public androidx.credentials.Credential getCredential();
-    property public final androidx.credentials.Credential credential;
-  }
-
-  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
-  }
-
-  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
-    ctor public GetPasswordOption();
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class PasswordCredential extends androidx.credentials.Credential {
-    ctor public PasswordCredential(String id, String password);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-    field public static final androidx.credentials.PasswordCredential.Companion Companion;
-    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
-  }
-
-  public static final class PasswordCredential.Companion {
-  }
-
-  @RequiresApi(34) public final class PrepareGetCredentialResponse {
-    method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
-    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
-    method public boolean isNullHandlesForTest();
-    property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
-    property public final boolean isNullHandlesForTest;
-    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
-  }
-
-  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
-    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
-  }
-
-  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
-    ctor public PrepareGetCredentialResponse.TestBuilder();
-    method public androidx.credentials.PrepareGetCredentialResponse build();
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-  }
-
-  public final class PublicKeyCredential extends androidx.credentials.Credential {
-    ctor public PublicKeyCredential(String authenticationResponseJson);
-    method public String getAuthenticationResponseJson();
-    property public final String authenticationResponseJson;
-    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
-    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
-  }
-
-  public static final class PublicKeyCredential.Companion {
-  }
-
-}
-
-package androidx.credentials.exceptions {
-
-  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialCustomException(String type);
-    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class ClearCredentialException extends java.lang.Exception {
-  }
-
-  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialInterruptedException();
-    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialProviderConfigurationException();
-    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnknownException();
-    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnsupportedException();
-    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCancellationException();
-    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCustomException(String type);
-    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class CreateCredentialException extends java.lang.Exception {
-  }
-
-  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialInterruptedException();
-    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialNoCreateOptionException();
-    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialProviderConfigurationException();
-    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnknownException();
-    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnsupportedException();
-    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCancellationException();
-    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCustomException(String type);
-    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class GetCredentialException extends java.lang.Exception {
-  }
-
-  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialInterruptedException();
-    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialProviderConfigurationException();
-    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnknownException();
-    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnsupportedException();
-    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public NoCredentialException();
-    ctor public NoCredentialException(optional CharSequence? errorMessage);
-  }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
-  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public AbortError();
-  }
-
-  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ConstraintError();
-  }
-
-  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataCloneError();
-  }
-
-  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataError();
-  }
-
-  public abstract class DomError {
-    ctor public DomError(String type);
-  }
-
-  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public EncodingError();
-  }
-
-  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public HierarchyRequestError();
-  }
-
-  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InUseAttributeError();
-  }
-
-  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidCharacterError();
-  }
-
-  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidModificationError();
-  }
-
-  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidNodeTypeError();
-  }
-
-  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidStateError();
-  }
-
-  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NamespaceError();
-  }
-
-  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NetworkError();
-  }
-
-  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NoModificationAllowedError();
-  }
-
-  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotAllowedError();
-  }
-
-  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotFoundError();
-  }
-
-  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotReadableError();
-  }
-
-  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotSupportedError();
-  }
-
-  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OperationError();
-  }
-
-  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OptOutError();
-  }
-
-  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public QuotaExceededError();
-  }
-
-  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ReadOnlyError();
-  }
-
-  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SecurityError();
-  }
-
-  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SyntaxError();
-  }
-
-  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TimeoutError();
-  }
-
-  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TransactionInactiveError();
-  }
-
-  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public UnknownError();
-  }
-
-  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public VersionError();
-  }
-
-  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public WrongDocumentError();
-  }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
-  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
-  }
-
-  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-  }
-
-}
-
-package androidx.credentials.provider {
-
-  public final class Action {
-    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
-    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.Action.Companion Companion;
-  }
-
-  public static final class Action.Builder {
-    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.Action build();
-    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
-  }
-
-  public static final class Action.Companion {
-    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-  }
-
-  public final class AuthenticationAction {
-    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
-    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
-  }
-
-  public static final class AuthenticationAction.Builder {
-    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.AuthenticationAction build();
-  }
-
-  public static final class AuthenticationAction.Companion {
-    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-  }
-
-  public abstract class BeginCreateCredentialRequest {
-    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getType();
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    property public final android.os.Bundle candidateQueryData;
-    property public final String type;
-    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginCreateCredentialResponse {
-    ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialResponse.Builder {
-    ctor public BeginCreateCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginCreateCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-  }
-
-  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public abstract class BeginGetCredentialOption {
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getId();
-    method public final String getType();
-    property public final android.os.Bundle candidateQueryData;
-    property public final String id;
-    property public final String type;
-  }
-
-  public final class BeginGetCredentialRequest {
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
-    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginGetCredentialResponse {
-    ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.Action> getActions();
-    method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
-    method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.Action> actions;
-    property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
-    property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialResponse.Builder {
-    ctor public BeginGetCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
-    method public androidx.credentials.provider.BeginGetCredentialResponse build();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginGetCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CallingAppInfo {
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
-    method public String? getOrigin(String privilegedAllowlist);
-    method public String getPackageName();
-    method public android.content.pm.SigningInfo getSigningInfo();
-    method public boolean isOriginPopulated();
-    property public final String packageName;
-    property public final android.content.pm.SigningInfo signingInfo;
-  }
-
-  @RequiresApi(26) public final class CreateEntry {
-    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
-    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-    method public CharSequence getAccountName();
-    method public CharSequence? getDescription();
-    method public android.graphics.drawable.Icon? getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public Integer? getPasswordCredentialCount();
-    method public android.app.PendingIntent getPendingIntent();
-    method public Integer? getPublicKeyCredentialCount();
-    method public Integer? getTotalCredentialCount();
-    method public boolean isAutoSelectAllowed();
-    property public final CharSequence accountName;
-    property public final CharSequence? description;
-    property public final android.graphics.drawable.Icon? icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
-  }
-
-  public static final class CreateEntry.Builder {
-    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.CreateEntry build();
-    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
-    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
-    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
-  }
-
-  public static final class CreateEntry.Companion {
-    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-  }
-
-  public abstract class CredentialEntry {
-    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public final CharSequence? getAffiliatedDomain();
-    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
-    method public final CharSequence getEntryGroupId();
-    method public final boolean isDefaultIconPreferredAsSingleProvider();
-    property public final CharSequence? affiliatedDomain;
-    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
-    property public final CharSequence entryGroupId;
-    property public final boolean isDefaultIconPreferredAsSingleProvider;
-    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
-  }
-
-  public static final class CredentialEntry.Companion {
-    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
-    ctor public CredentialProviderService();
-    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
-    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
-    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
-    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-  }
-
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    method public String getType();
-    method public CharSequence? getTypeDisplayName();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    property public String type;
-    property public final CharSequence? typeDisplayName;
-    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
-  }
-
-  public static final class CustomCredentialEntry.Builder {
-    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
-    method public androidx.credentials.provider.CustomCredentialEntry build();
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
-  }
-
-  public static final class CustomCredentialEntry.Companion {
-    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class IntentHandlerConverters {
-    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
-  }
-
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
-  }
-
-  public static final class PasswordCredentialEntry.Builder {
-    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
-    method public androidx.credentials.provider.PasswordCredentialEntry build();
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PasswordCredentialEntry.Companion {
-    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public final class PendingIntentHandler {
-    ctor public PendingIntentHandler();
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
-  }
-
-  public static final class PendingIntentHandler.Companion {
-    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-  }
-
-  public final class ProviderClearCredentialStateRequest {
-    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-  }
-
-  public final class ProviderCreateCredentialRequest {
-    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final androidx.credentials.CreateCredentialRequest callingRequest;
-  }
-
-  public final class ProviderGetCredentialRequest {
-    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-  }
-
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
-  }
-
-  public static final class PublicKeyCredentialEntry.Builder {
-    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PublicKeyCredentialEntry.Companion {
-    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class RemoteEntry {
-    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
-    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-    method public android.app.PendingIntent getPendingIntent();
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
-  }
-
-  public static final class RemoteEntry.Builder {
-    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.RemoteEntry build();
-  }
-
-  public static final class RemoteEntry.Companion {
-    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-  }
-
-}
-
diff --git a/credentials/credentials/api/restricted_1.3.0-beta02.txt b/credentials/credentials/api/restricted_1.3.0-beta02.txt
deleted file mode 100644
index a0d0eda..0000000
--- a/credentials/credentials/api/restricted_1.3.0-beta02.txt
+++ /dev/null
@@ -1,1001 +0,0 @@
-// Signature format: 4.0
-package androidx.credentials {
-
-  public final class ClearCredentialStateRequest {
-    ctor public ClearCredentialStateRequest();
-  }
-
-  public abstract class CreateCredentialRequest {
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getCredentialData();
-    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
-    method public final String? getOrigin();
-    method public final String getType();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    method public final boolean preferImmediatelyAvailableCredentials();
-    property public final android.os.Bundle candidateQueryData;
-    property public final android.os.Bundle credentialData;
-    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final String? origin;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final String type;
-    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo {
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
-    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
-    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-    method public CharSequence? getUserDisplayName();
-    method public CharSequence getUserId();
-    property public final CharSequence? userDisplayName;
-    property public final CharSequence userId;
-    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
-  }
-
-  public static final class CreateCredentialRequest.DisplayInfo.Companion {
-    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
-  }
-
-  public abstract class CreateCredentialResponse {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
-    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-  }
-
-  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
-  }
-
-  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePasswordRequest(String id, String password);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
-    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-  }
-
-  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePasswordResponse();
-  }
-
-  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
-    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
-    method public String getRegistrationResponseJson();
-    property public final String registrationResponseJson;
-  }
-
-  public abstract class Credential {
-    method public final android.os.Bundle getData();
-    method public final String getType();
-    property public final android.os.Bundle data;
-    property public final String type;
-  }
-
-  public interface CredentialManager {
-    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public static androidx.credentials.CredentialManager create(android.content.Context context);
-    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.CreateCredentialResponse>);
-    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
-    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation<? super androidx.credentials.GetCredentialResponse>);
-    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation<? super androidx.credentials.PrepareGetCredentialResponse>);
-    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    field public static final androidx.credentials.CredentialManager.Companion Companion;
-  }
-
-  public static final class CredentialManager.Companion {
-    method public androidx.credentials.CredentialManager create(android.content.Context context);
-  }
-
-  public interface CredentialManagerCallback<R, E> {
-    method public void onError(E e);
-    method public void onResult(R result);
-  }
-
-  public abstract class CredentialOption {
-    method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final android.os.Bundle getRequestData();
-    method public final String getType();
-    method public final int getTypePriorityHint();
-    method public final boolean isAutoSelectAllowed();
-    method public final boolean isSystemProviderRequired();
-    property public final java.util.Set<android.content.ComponentName> allowedProviders;
-    property public final android.os.Bundle candidateQueryData;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isSystemProviderRequired;
-    property public final android.os.Bundle requestData;
-    property public final String type;
-    property public final int typePriorityHint;
-    field public static final androidx.credentials.CredentialOption.Companion Companion;
-    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
-    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
-    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
-    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
-  }
-
-  public static final class CredentialOption.Companion {
-  }
-
-  public interface CredentialProvider {
-    method public boolean isAvailableOnDevice();
-    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.CreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.GetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback<androidx.credentials.PrepareGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-  }
-
-  public class CustomCredential extends androidx.credentials.Credential {
-    ctor public CustomCredential(String type, android.os.Bundle data);
-  }
-
-  public final class GetCredentialRequest {
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
-    ctor public GetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    method public String? getOrigin();
-    method public boolean getPreferIdentityDocUi();
-    method public android.content.ComponentName? getPreferUiBrandingComponentName();
-    method public boolean preferImmediatelyAvailableCredentials();
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-    property public final String? origin;
-    property public final boolean preferIdentityDocUi;
-    property public final boolean preferImmediatelyAvailableCredentials;
-    property public final android.content.ComponentName? preferUiBrandingComponentName;
-  }
-
-  public static final class GetCredentialRequest.Builder {
-    ctor public GetCredentialRequest.Builder();
-    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
-    method public androidx.credentials.GetCredentialRequest build();
-    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions);
-    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
-    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
-  }
-
-  public final class GetCredentialResponse {
-    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
-    method public androidx.credentials.Credential getCredential();
-    property public final androidx.credentials.Credential credential;
-  }
-
-  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders, optional int typePriorityHint);
-  }
-
-  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
-    ctor public GetPasswordOption();
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed);
-    ctor public GetPasswordOption(optional java.util.Set<java.lang.String> allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set<android.content.ComponentName> allowedProviders);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class PasswordCredential extends androidx.credentials.Credential {
-    ctor public PasswordCredential(String id, String password);
-    method public String getId();
-    method public String getPassword();
-    property public final String id;
-    property public final String password;
-    field public static final androidx.credentials.PasswordCredential.Companion Companion;
-    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
-  }
-
-  public static final class PasswordCredential.Companion {
-  }
-
-  @RequiresApi(34) public final class PrepareGetCredentialResponse {
-    method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
-    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasRemoteResultsDelegate();
-    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
-    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
-    method public boolean isNullHandlesForTest();
-    property public final kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? credentialTypeDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasAuthResultsDelegate;
-    property public final kotlin.jvm.functions.Function0<java.lang.Boolean>? hasRemoteResultsDelegate;
-    property public final boolean isNullHandlesForTest;
-    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
-  }
-
-  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
-    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
-  }
-
-  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
-    ctor public PrepareGetCredentialResponse.TestBuilder();
-    method public androidx.credentials.PrepareGetCredentialResponse build();
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1<? super java.lang.String,java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0<java.lang.Boolean> handler);
-  }
-
-  public final class PublicKeyCredential extends androidx.credentials.Credential {
-    ctor public PublicKeyCredential(String authenticationResponseJson);
-    method public String getAuthenticationResponseJson();
-    property public final String authenticationResponseJson;
-    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
-    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
-  }
-
-  public static final class PublicKeyCredential.Companion {
-  }
-
-}
-
-package androidx.credentials.exceptions {
-
-  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialCustomException(String type);
-    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class ClearCredentialException extends java.lang.Exception {
-  }
-
-  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialInterruptedException();
-    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialProviderConfigurationException();
-    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnknownException();
-    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
-    ctor public ClearCredentialUnsupportedException();
-    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCancellationException();
-    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCustomException(String type);
-    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class CreateCredentialException extends java.lang.Exception {
-  }
-
-  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialInterruptedException();
-    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialNoCreateOptionException();
-    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialProviderConfigurationException();
-    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnknownException();
-    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialUnsupportedException();
-    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCancellationException();
-    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCustomException(String type);
-    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
-    method public String getType();
-    property public String type;
-  }
-
-  public abstract class GetCredentialException extends java.lang.Exception {
-  }
-
-  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialInterruptedException();
-    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialProviderConfigurationException();
-    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnknownException();
-    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
-  }
-
-  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialUnsupportedException();
-    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
-  }
-
-  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public NoCredentialException();
-    ctor public NoCredentialException(optional CharSequence? errorMessage);
-  }
-
-}
-
-package androidx.credentials.exceptions.domerrors {
-
-  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public AbortError();
-  }
-
-  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ConstraintError();
-  }
-
-  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataCloneError();
-  }
-
-  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public DataError();
-  }
-
-  public abstract class DomError {
-    ctor public DomError(String type);
-  }
-
-  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public EncodingError();
-  }
-
-  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public HierarchyRequestError();
-  }
-
-  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InUseAttributeError();
-  }
-
-  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidCharacterError();
-  }
-
-  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidModificationError();
-  }
-
-  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidNodeTypeError();
-  }
-
-  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public InvalidStateError();
-  }
-
-  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NamespaceError();
-  }
-
-  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NetworkError();
-  }
-
-  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NoModificationAllowedError();
-  }
-
-  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotAllowedError();
-  }
-
-  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotFoundError();
-  }
-
-  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotReadableError();
-  }
-
-  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public NotSupportedError();
-  }
-
-  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OperationError();
-  }
-
-  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public OptOutError();
-  }
-
-  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public QuotaExceededError();
-  }
-
-  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public ReadOnlyError();
-  }
-
-  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SecurityError();
-  }
-
-  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public SyntaxError();
-  }
-
-  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TimeoutError();
-  }
-
-  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public TransactionInactiveError();
-  }
-
-  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public UnknownError();
-  }
-
-  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public VersionError();
-  }
-
-  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
-    ctor public WrongDocumentError();
-  }
-
-}
-
-package androidx.credentials.exceptions.publickeycredential {
-
-  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
-  }
-
-  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
-    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
-    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
-    property public final androidx.credentials.exceptions.domerrors.DomError domError;
-  }
-
-  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
-  }
-
-}
-
-package androidx.credentials.provider {
-
-  public final class Action {
-    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
-    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.Action.Companion Companion;
-  }
-
-  public static final class Action.Builder {
-    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.Action build();
-    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
-  }
-
-  public static final class Action.Companion {
-    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
-  }
-
-  public final class AuthenticationAction {
-    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
-    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTitle();
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence title;
-    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
-  }
-
-  public static final class AuthenticationAction.Builder {
-    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.AuthenticationAction build();
-  }
-
-  public static final class AuthenticationAction.Companion {
-    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
-  }
-
-  public abstract class BeginCreateCredentialRequest {
-    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getType();
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    property public final android.os.Bundle candidateQueryData;
-    property public final String type;
-    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
-    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginCreateCredentialResponse {
-    ctor public BeginCreateCredentialResponse(optional java.util.List<androidx.credentials.provider.CreateEntry> createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.CreateEntry> getCreateEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.CreateEntry> createEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginCreateCredentialResponse.Builder {
-    ctor public BeginCreateCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List<androidx.credentials.provider.CreateEntry> createEntries);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginCreateCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
-    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-  }
-
-  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
-    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public abstract class BeginGetCredentialOption {
-    method public final android.os.Bundle getCandidateQueryData();
-    method public final String getId();
-    method public final String getType();
-    property public final android.os.Bundle candidateQueryData;
-    property public final String id;
-    property public final String type;
-  }
-
-  public final class BeginGetCredentialRequest {
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions);
-    ctor public BeginGetCredentialRequest(java.util.List<? extends androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.BeginGetCredentialOption> getBeginGetCredentialOptions();
-    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
-    property public final java.util.List<androidx.credentials.provider.BeginGetCredentialOption> beginGetCredentialOptions;
-    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
-    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialRequest.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
-    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
-  }
-
-  public final class BeginGetCredentialResponse {
-    ctor public BeginGetCredentialResponse(optional java.util.List<? extends androidx.credentials.provider.CredentialEntry> credentialEntries, optional java.util.List<androidx.credentials.provider.Action> actions, optional java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
-    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-    method public java.util.List<androidx.credentials.provider.Action> getActions();
-    method public java.util.List<androidx.credentials.provider.AuthenticationAction> getAuthenticationActions();
-    method public java.util.List<androidx.credentials.provider.CredentialEntry> getCredentialEntries();
-    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
-    property public final java.util.List<androidx.credentials.provider.Action> actions;
-    property public final java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationActions;
-    property public final java.util.List<androidx.credentials.provider.CredentialEntry> credentialEntries;
-    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
-    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
-  }
-
-  public static final class BeginGetCredentialResponse.Builder {
-    ctor public BeginGetCredentialResponse.Builder();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
-    method public androidx.credentials.provider.BeginGetCredentialResponse build();
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List<androidx.credentials.provider.Action> actions);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List<androidx.credentials.provider.AuthenticationAction> authenticationEntries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List<? extends androidx.credentials.provider.CredentialEntry> entries);
-    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
-  }
-
-  public static final class BeginGetCredentialResponse.Companion {
-    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
-  }
-
-  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
-  }
-
-  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPasswordOption(java.util.Set<java.lang.String> allowedUserIds, android.os.Bundle candidateQueryData, String id);
-    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
-    method public java.util.Set<java.lang.String> getAllowedUserIds();
-    property public final java.util.Set<java.lang.String> allowedUserIds;
-  }
-
-  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
-    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
-    method public byte[]? getClientDataHash();
-    method public String getRequestJson();
-    property public final byte[]? clientDataHash;
-    property public final String requestJson;
-  }
-
-  public final class CallingAppInfo {
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
-    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
-    method public String? getOrigin(String privilegedAllowlist);
-    method public String getPackageName();
-    method public android.content.pm.SigningInfo getSigningInfo();
-    method public boolean isOriginPopulated();
-    property public final String packageName;
-    property public final android.content.pm.SigningInfo signingInfo;
-  }
-
-  @RequiresApi(26) public final class CreateEntry {
-    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
-    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-    method public CharSequence getAccountName();
-    method public CharSequence? getDescription();
-    method public android.graphics.drawable.Icon? getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public Integer? getPasswordCredentialCount();
-    method public android.app.PendingIntent getPendingIntent();
-    method public Integer? getPublicKeyCredentialCount();
-    method public Integer? getTotalCredentialCount();
-    method public boolean isAutoSelectAllowed();
-    property public final CharSequence accountName;
-    property public final CharSequence? description;
-    property public final android.graphics.drawable.Icon? icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
-  }
-
-  public static final class CreateEntry.Builder {
-    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.CreateEntry build();
-    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
-    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
-    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
-    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
-  }
-
-  public static final class CreateEntry.Companion {
-    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
-  }
-
-  public abstract class CredentialEntry {
-    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public final CharSequence? getAffiliatedDomain();
-    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
-    method public final CharSequence getEntryGroupId();
-    method public final boolean isDefaultIconPreferredAsSingleProvider();
-    property public final CharSequence? affiliatedDomain;
-    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
-    property public final CharSequence entryGroupId;
-    property public final boolean isDefaultIconPreferredAsSingleProvider;
-    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
-  }
-
-  public static final class CredentialEntry.Companion {
-    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
-    ctor public CredentialProviderService();
-    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginCreateCredentialResponse,android.credentials.CreateCredentialException> callback);
-    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginCreateCredentialResponse,androidx.credentials.exceptions.CreateCredentialException> callback);
-    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException> callback);
-    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<androidx.credentials.provider.BeginGetCredentialResponse,androidx.credentials.exceptions.GetCredentialException> callback);
-    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException> callback);
-    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
-  }
-
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence? getSubtitle();
-    method public CharSequence getTitle();
-    method public String getType();
-    method public CharSequence? getTypeDisplayName();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence? subtitle;
-    property public final CharSequence title;
-    property public String type;
-    property public final CharSequence? typeDisplayName;
-    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
-  }
-
-  public static final class CustomCredentialEntry.Builder {
-    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
-    method public androidx.credentials.provider.CustomCredentialEntry build();
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
-    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
-  }
-
-  public static final class CustomCredentialEntry.Companion {
-    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class IntentHandlerConverters {
-    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
-    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
-  }
-
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
-  }
-
-  public static final class PasswordCredentialEntry.Builder {
-    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
-    method public androidx.credentials.provider.PasswordCredentialEntry build();
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PasswordCredentialEntry.Companion {
-    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  @RequiresApi(34) public final class PendingIntentHandler {
-    ctor public PendingIntentHandler();
-    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
-  }
-
-  public static final class PendingIntentHandler.Companion {
-    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
-    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
-    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
-    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
-    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
-    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
-    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
-  }
-
-  public final class ProviderClearCredentialStateRequest {
-    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-  }
-
-  public final class ProviderCreateCredentialRequest {
-    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final androidx.credentials.CreateCredentialRequest callingRequest;
-  }
-
-  public final class ProviderGetCredentialRequest {
-    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
-    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
-    method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
-    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
-    property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
-  }
-
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
-    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
-    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
-    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-    method public CharSequence? getDisplayName();
-    method public android.graphics.drawable.Icon getIcon();
-    method public java.time.Instant? getLastUsedTime();
-    method public android.app.PendingIntent getPendingIntent();
-    method public CharSequence getTypeDisplayName();
-    method public CharSequence getUsername();
-    method public boolean hasDefaultIcon();
-    method public boolean isAutoSelectAllowed();
-    method public boolean isAutoSelectAllowedFromOption();
-    property public final CharSequence? displayName;
-    property public final boolean hasDefaultIcon;
-    property public final android.graphics.drawable.Icon icon;
-    property public final boolean isAutoSelectAllowed;
-    property public final boolean isAutoSelectAllowedFromOption;
-    property public final java.time.Instant? lastUsedTime;
-    property public final android.app.PendingIntent pendingIntent;
-    property public final CharSequence typeDisplayName;
-    property public final CharSequence username;
-    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
-  }
-
-  public static final class PublicKeyCredentialEntry.Builder {
-    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
-    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
-  }
-
-  public static final class PublicKeyCredentialEntry.Companion {
-    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
-  }
-
-  public final class RemoteEntry {
-    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
-    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-    method public android.app.PendingIntent getPendingIntent();
-    property public final android.app.PendingIntent pendingIntent;
-    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
-  }
-
-  public static final class RemoteEntry.Builder {
-    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
-    method public androidx.credentials.provider.RemoteEntry build();
-  }
-
-  public static final class RemoteEntry.Companion {
-    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
-  }
-
-}
-
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index a0d0eda..8b50742 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -133,6 +133,11 @@
     method public void onResult(R result);
   }
 
+  public final class CredentialManagerViewHandler {
+    method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
+    method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
+  }
+
   public abstract class CredentialOption {
     method public final java.util.Set<android.content.ComponentName> getAllowedProviders();
     method public final android.os.Bundle getCandidateQueryData();
@@ -245,6 +250,14 @@
   public static final class PasswordCredential.Companion {
   }
 
+  public final class PendingGetCredentialRequest {
+    ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1<? super androidx.credentials.GetCredentialResponse,kotlin.Unit> callback);
+    method public kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> getCallback();
+    method public androidx.credentials.GetCredentialRequest getRequest();
+    property public final kotlin.jvm.functions.Function1<androidx.credentials.GetCredentialResponse,kotlin.Unit> callback;
+    property public final androidx.credentials.GetCredentialRequest request;
+  }
+
   @RequiresApi(34) public final class PrepareGetCredentialResponse {
     method public kotlin.jvm.functions.Function1<java.lang.String,java.lang.Boolean>? getCredentialTypeDelegate();
     method public kotlin.jvm.functions.Function0<java.lang.Boolean>? getHasAuthResultsDelegate();
@@ -591,6 +604,21 @@
     method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
   }
 
+  public final class AuthenticationError {
+    ctor public AuthenticationError(int errorCode);
+    ctor public AuthenticationError(int errorCode, optional CharSequence? errorMsg);
+    method public int getErrorCode();
+    method public CharSequence? getErrorMsg();
+    property public final int errorCode;
+    property public final CharSequence? errorMsg;
+  }
+
+  public final class AuthenticationResult {
+    ctor public AuthenticationResult(int authenticationType);
+    method public int getAuthenticationType();
+    property public final int authenticationType;
+  }
+
   public abstract class BeginCreateCredentialRequest {
     ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
     method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
@@ -729,6 +757,33 @@
     property public final String requestJson;
   }
 
+  @RequiresApi(35) public final class BiometricPromptData {
+    ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject);
+    ctor public BiometricPromptData(optional androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject, optional int allowedAuthenticators);
+    method public int getAllowedAuthenticators();
+    method public androidx.biometric.BiometricPrompt.CryptoObject? getCryptoObject();
+    property public final int allowedAuthenticators;
+    property public final androidx.biometric.BiometricPrompt.CryptoObject? cryptoObject;
+  }
+
+  public static final class BiometricPromptData.Builder {
+    ctor public BiometricPromptData.Builder();
+    method public androidx.credentials.provider.BiometricPromptData build();
+    method public androidx.credentials.provider.BiometricPromptData.Builder setAllowedAuthenticators(int allowedAuthenticators);
+    method public androidx.credentials.provider.BiometricPromptData.Builder setCryptoObject(androidx.biometric.BiometricPrompt.CryptoObject cryptoObject);
+  }
+
+  public final class BiometricPromptResult {
+    ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationError authenticationError);
+    ctor public BiometricPromptResult(androidx.credentials.provider.AuthenticationResult authenticationResult);
+    method public androidx.credentials.provider.AuthenticationError? getAuthenticationError();
+    method public androidx.credentials.provider.AuthenticationResult? getAuthenticationResult();
+    method public boolean isSuccessful();
+    property public final androidx.credentials.provider.AuthenticationError? authenticationError;
+    property public final androidx.credentials.provider.AuthenticationResult? authenticationResult;
+    property public final boolean isSuccessful;
+  }
+
   public final class CallingAppInfo {
     ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
     ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
@@ -740,10 +795,12 @@
     property public final android.content.pm.SigningInfo signingInfo;
   }
 
-  @RequiresApi(26) public final class CreateEntry {
+  @RequiresApi(23) public final class CreateEntry {
     ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
     method public CharSequence getAccountName();
+    method public androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
     method public CharSequence? getDescription();
     method public android.graphics.drawable.Icon? getIcon();
     method public java.time.Instant? getLastUsedTime();
@@ -753,6 +810,7 @@
     method public Integer? getTotalCredentialCount();
     method public boolean isAutoSelectAllowed();
     property public final CharSequence accountName;
+    property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
     property public final CharSequence? description;
     property public final android.graphics.drawable.Icon? icon;
     property public final boolean isAutoSelectAllowed;
@@ -765,6 +823,7 @@
     ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
     method public androidx.credentials.provider.CreateEntry build();
     method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CreateEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
     method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
     method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
@@ -781,10 +840,12 @@
     method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public final CharSequence? getAffiliatedDomain();
     method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
+    method public final androidx.credentials.provider.BiometricPromptData? getBiometricPromptData();
     method public final CharSequence getEntryGroupId();
     method public final boolean isDefaultIconPreferredAsSingleProvider();
     property public final CharSequence? affiliatedDomain;
     property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
+    property public final androidx.credentials.provider.BiometricPromptData? biometricPromptData;
     property public final CharSequence entryGroupId;
     property public final boolean isDefaultIconPreferredAsSingleProvider;
     field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
@@ -804,9 +865,10 @@
     method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver<java.lang.Void?,androidx.credentials.exceptions.ClearCredentialException> callback);
   }
 
-  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public android.graphics.drawable.Icon getIcon();
     method public java.time.Instant? getLastUsedTime();
@@ -835,6 +897,7 @@
     ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
     method public androidx.credentials.provider.CustomCredentialEntry build();
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.CustomCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
     method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -855,9 +918,10 @@
     method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
   }
 
-  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public CharSequence? getDisplayName();
     method public android.graphics.drawable.Icon getIcon();
@@ -885,6 +949,7 @@
     method public androidx.credentials.provider.PasswordCredentialEntry build();
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PasswordCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
     method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
@@ -927,23 +992,30 @@
 
   public final class ProviderCreateCredentialRequest {
     ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+    method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
     method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
     method public androidx.credentials.CreateCredentialRequest getCallingRequest();
+    property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
     property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
     property public final androidx.credentials.CreateCredentialRequest callingRequest;
   }
 
   public final class ProviderGetCredentialRequest {
     ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
+    ctor public ProviderGetCredentialRequest(java.util.List<? extends androidx.credentials.CredentialOption> credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo, optional androidx.credentials.provider.BiometricPromptResult? biometricPromptResult);
+    method public androidx.credentials.provider.BiometricPromptResult? getBiometricPromptResult();
     method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
     method public java.util.List<androidx.credentials.CredentialOption> getCredentialOptions();
+    property public final androidx.credentials.provider.BiometricPromptResult? biometricPromptResult;
     property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
     property public final java.util.List<androidx.credentials.CredentialOption> credentialOptions;
   }
 
-  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
+  @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
     ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
     ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider, optional androidx.credentials.provider.BiometricPromptData? biometricPromptData);
     method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
     method public CharSequence? getDisplayName();
     method public android.graphics.drawable.Icon getIcon();
@@ -970,6 +1042,7 @@
     ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
     method public androidx.credentials.provider.PublicKeyCredentialEntry build();
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
+    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setBiometricPromptData(androidx.credentials.provider.BiometricPromptData biometricPromptData);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
     method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index da177eb..37791f3 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -31,8 +31,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.5.0")
+    api("androidx.biometric:biometric-ktx:1.4.0-alpha01")
     api(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core:1.15.0-alpha01")
 
     androidTestImplementation("androidx.activity:activity:1.2.0")
     androidTestImplementation(libs.junit)
@@ -41,6 +43,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":core:core"))
     androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation(libs.kotlinCoroutinesAndroid)
     androidTestImplementation(project(":internal-testutils-runtime"), {
@@ -49,6 +52,7 @@
 }
 
 android {
+    compileSdk = 35
     namespace "androidx.credentials"
 
     defaultConfig {
diff --git a/credentials/credentials/samples/build.gradle b/credentials/credentials/samples/build.gradle
index b0b0178..fa9238c 100644
--- a/credentials/credentials/samples/build.gradle
+++ b/credentials/credentials/samples/build.gradle
@@ -30,6 +30,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.credentials.samples"
 
     defaultConfig {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
index 81f3196..390b9db 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
@@ -116,7 +116,7 @@
         assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(expectedDefaultProvider);
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     public void constructFromBundle_success() {
         String expectedUserId = "userId";
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
index ab9bb18..a4a07e29 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
@@ -101,7 +101,7 @@
         assertThat(displayInfo.preferDefaultProvider).isEqualTo(expectedDefaultProvider)
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     fun constructFromBundle_success() {
         val expectedUserId = "userId"
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
index 1e8d7ad..a8579df 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -139,7 +139,7 @@
         assertThat(request.getPassword()).isEqualTo(passwordExpected);
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @SuppressWarnings("deprecation") // bundle.get(key)
     @Test
     public void getter_frameworkProperties() {
@@ -191,7 +191,7 @@
         ).isEqualTo(R.drawable.ic_password);
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     public void frameworkConversion_success() {
         String idExpected = "id";
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
index 3f9e8dd..db25fc5 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -126,7 +126,7 @@
         assertThat(request.password).isEqualTo(passwordExpected)
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Suppress("DEPRECATION") // bundle.get(key)
     @Test
     fun getter_frameworkProperties() {
@@ -189,7 +189,7 @@
             .isEqualTo(R.drawable.ic_password)
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     fun frameworkConversion_success() {
         val idExpected = "id"
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index cb095bb..893d0f9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -154,7 +154,7 @@
         assertThat(testJsonActual).isEqualTo(testJsonExpected);
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @SuppressWarnings("deprecation") // bundle.get(key)
     @Test
     public void getter_frameworkProperties_success() {
@@ -210,7 +210,7 @@
         ).isEqualTo(R.drawable.ic_passkey);
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     public void frameworkConversion_success() {
         byte[] clientDataHashExpected = "hash".getBytes();
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index 4715c85..cd3b53c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -140,7 +140,7 @@
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Suppress("DEPRECATION") // bundle.get(key)
     @Test
     fun getter_frameworkProperties_success() {
@@ -211,7 +211,7 @@
             .isEqualTo(R.drawable.ic_passkey)
     }
 
-    @SdkSuppress(minSdkVersion = 28)
+    @SdkSuppress(minSdkVersion = 34)
     @Test
     fun frameworkConversion_success() {
         val clientDataHashExpected = "hash".toByteArray()
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java
new file mode 100644
index 0000000..06f71a5
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.credentials.Credential;
+import android.os.OutcomeReceiver;
+import android.widget.EditText;
+
+import androidx.annotation.RequiresApi;
+import androidx.credentials.internal.FrameworkImplHelper;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import kotlin.Unit;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+public class CredentialManagerViewHandlerJavaTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    private static final GetCredentialRequest GET_CRED_PASSWORD_REQ =
+            new GetCredentialRequest.Builder()
+                    .setCredentialOptions(Collections.singletonList(
+                            new GetPasswordOption())).build();
+    private static final android.credentials.GetCredentialRequest GET_CRED_PASSWORD_FRAMEWORK_REQ =
+            FrameworkImplHelper.convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ);
+
+    @Test
+    @RequiresApi(35)
+    public void setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
+        EditText editText = new EditText(mContext);
+
+        PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
+                GET_CRED_PASSWORD_REQ,
+                (response) -> Unit.INSTANCE);
+
+        CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
+                pendingGetCredentialRequest);
+
+        assertNotNull(editText.getPendingCredentialRequest());
+        TestUtilsKt.equals(editText.getPendingCredentialRequest(),
+                GET_CRED_PASSWORD_FRAMEWORK_REQ);
+        assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
+                OutcomeReceiver.class
+        );
+    }
+
+    @Test
+    @RequiresApi(35)
+    public void setPendingCredentialRequest_callbackInvokedSuccessfully()
+            throws InterruptedException {
+        CountDownLatch latch1 = new CountDownLatch(1);
+        AtomicReference<GetCredentialResponse> getCredentialResponse = new AtomicReference<>();
+        EditText editText = new EditText(mContext);
+
+        PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
+                GET_CRED_PASSWORD_REQ,
+                (response) -> {
+                    getCredentialResponse.set(response);
+                    latch1.countDown();
+                    return Unit.INSTANCE;
+                });
+
+        CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
+                pendingGetCredentialRequest);
+
+        assertNotNull(editText.getPendingCredentialRequest());
+        TestUtilsKt.equals(editText.getPendingCredentialRequest(), GET_CRED_PASSWORD_FRAMEWORK_REQ);
+        assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
+                OutcomeReceiver.class
+        );
+
+        PasswordCredential passwordCredential = new PasswordCredential("id", "password");
+        android.credentials.GetCredentialResponse frameworkPasswordResponse =
+                new android.credentials.GetCredentialResponse(new Credential(
+                        passwordCredential.getType(), passwordCredential.getData()));
+        assertNotNull(editText.getPendingCredentialCallback());
+        editText.getPendingCredentialCallback().onResult(frameworkPasswordResponse);
+        latch1.await(50L, TimeUnit.MILLISECONDS);
+
+        assertThat(getCredentialResponse.get()).isNotNull();
+        GetCredentialResponse expectedGetCredentialResponse = FrameworkImplHelper
+                .convertGetResponseToJetpackClass(frameworkPasswordResponse);
+        TestUtilsKt.equals(expectedGetCredentialResponse, getCredentialResponse.get());
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
new file mode 100644
index 0000000..d0e4b6b
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials
+
+import android.content.Context
+import android.credentials.Credential
+import android.os.OutcomeReceiver
+import android.widget.EditText
+import androidx.annotation.RequiresApi
+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetRequestToFrameworkClass
+import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetResponseToJetpackClass
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+class CredentialManagerViewHandlerTest {
+    private val mContext: Context = ApplicationProvider.getApplicationContext()
+
+    companion object {
+        private val GET_CRED_PASSWORD_REQ =
+            GetCredentialRequest.Builder().setCredentialOptions(listOf(GetPasswordOption())).build()
+        private val GET_CRED_PASSWORD_FRAMEWORK_REQ =
+            convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ)
+    }
+
+    @Test
+    @RequiresApi(35)
+    fun setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
+        val editText = EditText(mContext)
+        val pendingGetCredentialRequest =
+            PendingGetCredentialRequest(GET_CRED_PASSWORD_REQ) { _: GetCredentialResponse? -> }
+
+        editText.pendingGetCredentialRequest = pendingGetCredentialRequest
+
+        equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)
+        assertThat(editText.pendingCredentialCallback).isInstanceOf(OutcomeReceiver::class.java)
+    }
+
+    @Test
+    @RequiresApi(35)
+    @Throws(InterruptedException::class)
+    fun setPendingCredentialRequest_callbackInvokedSuccessfully() {
+        val latch1 = CountDownLatch(1)
+        val getCredentialResponse = AtomicReference<GetCredentialResponse>()
+        val editText = EditText(mContext)
+
+        editText.pendingGetCredentialRequest =
+            PendingGetCredentialRequest(GET_CRED_PASSWORD_REQ) { response ->
+                getCredentialResponse.set(response)
+                latch1.countDown()
+            }
+
+        equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)
+        assertThat(editText.pendingCredentialCallback).isInstanceOf(OutcomeReceiver::class.java)
+
+        val passwordCredential = PasswordCredential("id", "password")
+        val frameworkPasswordResponse =
+            android.credentials.GetCredentialResponse(
+                Credential(passwordCredential.type, passwordCredential.data)
+            )
+
+        editText.pendingCredentialCallback!!.onResult(frameworkPasswordResponse)
+        latch1.await(50L, TimeUnit.MILLISECONDS)
+
+        assertThat(getCredentialResponse.get()).isNotNull()
+        val expectedGetCredentialResponse =
+            convertGetResponseToJetpackClass(frameworkPasswordResponse)
+        equals(expectedGetCredentialResponse, getCredentialResponse.get())
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
new file mode 100644
index 0000000..3d43bfd6
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.filters.SdkSuppress;
+
+import kotlin.Unit;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+public class PendingGetCredentialRequestJavaTest {
+    @Test
+    @RequiresApi(35)
+    public void constructor_setAndGetRequestThroughViewTag() {
+        GetCredentialRequest request = new GetCredentialRequest.Builder()
+                .setCredentialOptions(Collections.singletonList(new GetPasswordOption()))
+                .build();
+        PendingGetCredentialRequest pendingGetCredentialRequest =
+                new PendingGetCredentialRequest(request,
+                        (response) -> Unit.INSTANCE);
+
+        assertThat(pendingGetCredentialRequest.getRequest())
+                .isSameInstanceAs(request);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
new file mode 100644
index 0000000..1e3dad7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials
+
+import androidx.annotation.RequiresApi
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
+class PendingGetCredentialRequestTest {
+
+    @Test
+    @RequiresApi(35)
+    fun constructor_setAndGetRequestThroughViewTag() {
+        val request =
+            GetCredentialRequest.Builder().setCredentialOptions(listOf(GetPasswordOption())).build()
+
+        val pendingGetCredentialRequest =
+            PendingGetCredentialRequest(request) { _: GetCredentialResponse? -> }
+
+        assertThat(pendingGetCredentialRequest.request).isSameInstanceAs(request)
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
index c486318..37479ec 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
@@ -16,10 +16,16 @@
 
 package androidx.credentials
 
+import android.content.pm.SigningInfo
 import android.graphics.drawable.Icon
 import android.os.Build
 import android.os.Bundle
+import androidx.annotation.RequiresApi
 import androidx.credentials.provider.CallingAppInfo
+import androidx.credentials.provider.ProviderCreateCredentialRequest
+import androidx.credentials.provider.ProviderGetCredentialRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
 
 /** True if the two Bundles contain the same elements, and false otherwise. */
 @Suppress("DEPRECATION")
@@ -85,3 +91,115 @@
 fun equals(a: CallingAppInfo, b: CallingAppInfo): Boolean {
     return a.packageName == b.packageName && a.origin == b.origin
 }
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+    createCredentialRequest: android.service.credentials.CreateCredentialRequest,
+    request: ProviderCreateCredentialRequest
+) {
+    assertThat(createCredentialRequest.type).isEqualTo(request.callingRequest.type)
+    equals(createCredentialRequest.data, request.callingRequest.credentialData)
+    Assert.assertEquals(
+        createCredentialRequest.callingAppInfo.packageName,
+        request.callingAppInfo.packageName
+    )
+    Assert.assertEquals(
+        createCredentialRequest.callingAppInfo.origin,
+        request.callingAppInfo.origin
+    )
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+    getCredentialRequest: android.service.credentials.GetCredentialRequest,
+    request: ProviderGetCredentialRequest
+) {
+    Assert.assertEquals(
+        getCredentialRequest.callingAppInfo.packageName,
+        request.callingAppInfo.packageName
+    )
+    Assert.assertEquals(getCredentialRequest.callingAppInfo.origin, request.callingAppInfo.origin)
+    equals(getCredentialRequest.credentialOptions, request.credentialOptions)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private fun equals(
+    credentialOptions: List<android.credentials.CredentialOption>,
+    credentialOptions1: List<CredentialOption>
+) {
+    assertThat(credentialOptions.size).isEqualTo(credentialOptions1.size)
+    for (i in credentialOptions.indices) {
+        equals(credentialOptions[i], credentialOptions1[i])
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+    frameworkRequest1: android.credentials.GetCredentialRequest,
+    frameworkRequest2: android.credentials.GetCredentialRequest
+) {
+    equals(frameworkRequest1.data, frameworkRequest2.data)
+    credentialOptionsEqual(frameworkRequest1.credentialOptions, frameworkRequest2.credentialOptions)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private fun credentialOptionsEqual(
+    credentialOptions1: List<android.credentials.CredentialOption>,
+    credentialOptions2: List<android.credentials.CredentialOption>
+) {
+    assertThat(credentialOptions1.size).isEqualTo(credentialOptions2.size)
+    for (i in credentialOptions1.indices) {
+        equals(credentialOptions1[i], credentialOptions2[i])
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+    credentialOption: android.credentials.CredentialOption,
+    credentialOption1: CredentialOption
+) {
+    assertThat(credentialOption.type).isEqualTo(credentialOption1.type)
+    assertThat(credentialOption.isSystemProviderRequired)
+        .isEqualTo(credentialOption1.isSystemProviderRequired)
+    equals(credentialOption.credentialRetrievalData, credentialOption1.requestData)
+    equals(credentialOption.candidateQueryData, credentialOption1.candidateQueryData)
+    assertThat(credentialOption.allowedProviders).isEqualTo(credentialOption1.allowedProviders)
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun setUpCreatePasswordRequest(): android.service.credentials.CreateCredentialRequest {
+    val passwordReq: CreateCredentialRequest =
+        CreatePasswordRequest("test-user-id", "test-password")
+    val request =
+        android.service.credentials.CreateCredentialRequest(
+            android.service.credentials.CallingAppInfo("calling_package", SigningInfo()),
+            PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+            passwordReq.credentialData
+        )
+    return request
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun equals(
+    credentialOption1: android.credentials.CredentialOption,
+    credentialOption2: android.credentials.CredentialOption
+) {
+    equals(credentialOption1.candidateQueryData, credentialOption2.candidateQueryData)
+    equals(credentialOption1.credentialRetrievalData, credentialOption2.credentialRetrievalData)
+    assertThat(credentialOption1.type).isEqualTo(credentialOption2.type)
+    assertThat(credentialOption1.allowedProviders).isEqualTo(credentialOption2.allowedProviders)
+    assertThat(credentialOption1.isSystemProviderRequired)
+        .isEqualTo(credentialOption2.isSystemProviderRequired)
+}
+
+fun equals(
+    getCredentialResponse1: GetCredentialResponse,
+    getCredentialResponse2: GetCredentialResponse
+) {
+    equals(getCredentialResponse1.credential, getCredentialResponse2.credential)
+}
+
+fun equals(credential1: Credential, credential2: Credential) {
+    assertThat(credential1.type).isEqualTo(credential2.type)
+    equals(credential1.data, credential2.data)
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
new file mode 100644
index 0000000..1e5593c
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider;
+
+import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_ALLOWED_AUTHENTICATORS;
+import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_CRYPTO_OP_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+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 javax.crypto.NullCipher;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 35)
+public class BiometricPromptDataJavaTest {
+
+    private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = new
+            BiometricPrompt.CryptoObject(new NullCipher());
+
+    private static final long DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L;
+
+    private static final  int TEST_ALLOWED_AUTHENTICATOR = BiometricManager.Authenticators
+            .BIOMETRIC_STRONG;
+
+    @Test
+    public void construct_cryptoObjectStrongAllowedAuthenticator_success() {
+        BiometricPromptData biometricPromptData = new BiometricPromptData(
+                /*cryptoObject=*/TEST_CRYPTO_OBJECT,
+                /*allowedAuthenticators=*/TEST_ALLOWED_AUTHENTICATOR
+        );
+
+        assertThat(biometricPromptData.getAllowedAuthenticators())
+                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+        assertThat(biometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
+    }
+
+    @Test
+    public void construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
+        int expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+        BiometricPromptData biometricPromptData = new BiometricPromptData.Builder().build();
+
+        assertThat(biometricPromptData.getCryptoObject()).isNull();
+        assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(expectedAuthenticator);
+    }
+
+    @Test
+    public void construct_cryptoObjectExistsAuthenticatorNotProvided__defaultThrowsIAE() {
+        assertThrows("Expected cryptoObject without strong authenticator to throw "
+                        + "IllegalArgumentException",
+                IllegalArgumentException.class,
+                () -> new BiometricPromptData.Builder()
+                        .setCryptoObject(TEST_CRYPTO_OBJECT).build()
+        );
+    }
+
+    @Test
+    public void construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
+        BiometricPromptData biometricPromptData = new BiometricPromptData(
+                /*cryptoObject=*/null,
+                /*allowedAuthenticator=*/TEST_ALLOWED_AUTHENTICATOR
+        );
+
+        assertThat(biometricPromptData.getCryptoObject()).isNull();
+        assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(
+                TEST_ALLOWED_AUTHENTICATOR);
+    }
+
+    @Test
+    public void construct_authenticatorNotAccepted_throwsIAE() {
+        assertThrows("Expected invalid allowed authenticator to throw "
+                        + "IllegalArgumentException",
+                IllegalArgumentException.class,
+                () -> new BiometricPromptData(
+                        /*cryptoObject=*/null,
+                        /*allowedAuthenticator=*/Integer.MIN_VALUE
+                )
+        );
+    }
+
+    @Test
+    public void build_requiredParamsOnly_success() {
+        int expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;
+
+        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder().build();
+
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+                expectedAllowedAuthenticators);
+        assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
+    }
+
+    @Test
+    public void build_setCryptoObjectWithStrongAuthenticator_success() {
+        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
+                .setCryptoObject(TEST_CRYPTO_OBJECT)
+                .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
+
+        assertThat(actualBiometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+    }
+
+    @Test
+    public void build_setAllowedAuthenticator_success() {
+        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
+                .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
+
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
+    }
+
+    @SdkSuppress(maxSdkVersion = 34)
+    @Test
+    public void fromBundle_validAllowedAuthenticator_success() {
+        Bundle inputBundle = new Bundle();
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
+
+        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+        assertThat(actualBiometricPromptData).isNotNull();
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+                TEST_ALLOWED_AUTHENTICATOR);
+        assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
+    }
+
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    public void fromBundle_validAllowedAuthenticatorAboveApi35_success() {
+        int expectedOpId = Integer.MIN_VALUE;
+        Bundle inputBundle = new Bundle();
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
+        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+        assertThat(actualBiometricPromptData).isNotNull();
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
+                TEST_ALLOWED_AUTHENTICATOR);
+        assertThat(actualBiometricPromptData.getCryptoObject()).isNotNull();
+        assertThat(actualBiometricPromptData.getCryptoObject().hashCode())
+                .isEqualTo(expectedOpId);
+    }
+
+    @Test
+    public void fromBundle_unrecognizedAllowedAuthenticator_success() {
+        int expectedOpId = Integer.MIN_VALUE;
+        Bundle inputBundle = new Bundle();
+        int unrecognizedAuthenticator = Integer.MAX_VALUE;
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator);
+        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+        assertThat(actualBiometricPromptData).isNotNull();
+        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
+                .isEqualTo(unrecognizedAuthenticator);
+
+    }
+
+    @Test
+    public void fromBundle_invalidBundleKey_nullBiometricPromptData() {
+        int expectedOpId = Integer.MIN_VALUE;
+        Bundle inputBundle = new Bundle();
+        int unrecognizedAuthenticator = Integer.MAX_VALUE;
+        inputBundle.putInt("invalidKey", unrecognizedAuthenticator);
+        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
+
+        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
+
+        assertThat(actualBiometricPromptData).isNull();
+    }
+
+    @SdkSuppress(maxSdkVersion = 34)
+    @Test
+    public void toBundle_success() {
+        BiometricPromptData testBiometricPromptData = new BiometricPromptData(/*cryptoObject=*/null,
+                TEST_ALLOWED_AUTHENTICATOR);
+
+        Bundle actualBundle = BiometricPromptData.toBundle(
+                testBiometricPromptData);
+
+        assertThat(actualBundle).isNotNull();
+        assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
+                TEST_ALLOWED_AUTHENTICATOR
+        );
+        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(
+                DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID);
+    }
+
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    public void toBundle_api35AndAboveWithOpId_success() {
+        BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,
+                TEST_ALLOWED_AUTHENTICATOR);
+        long expectedOpId = TEST_CRYPTO_OBJECT.hashCode();
+
+        Bundle actualBundle = BiometricPromptData.toBundle(
+                testBiometricPromptData);
+
+        assertThat(actualBundle).isNotNull();
+        assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
+                TEST_ALLOWED_AUTHENTICATOR
+        );
+        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);
+    }
+
+    @Test
+    public void build_setInvalidAllowedAuthenticator_throwsIAE() {
+        assertThrows("Expected invalid allowed authenticator to throw "
+                        + "IllegalArgumentException",
+                IllegalArgumentException.class,
+                () -> new BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
+        );
+    }
+
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
new file mode 100644
index 0000000..399a783
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import android.os.Bundle
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
+import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID
+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 javax.crypto.NullCipher
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 35)
+@SmallTest
+class BiometricPromptDataTest {
+    @Test
+    fun construct_cryptoObjectStrongAllowedAuthenticator_success() {
+        val biometricPromptData =
+            BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+
+        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+        assertThat(biometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
+    }
+
+    @Test
+    fun construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
+        val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK
+
+        val biometricPromptData = BiometricPromptData()
+
+        assertThat(biometricPromptData.cryptoObject).isNull()
+        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
+    }
+
+    @Test
+    fun construct_cryptoObjectExistsAuthenticatorNotProvided_defaultWeakAuthenticatorThrowsIAE() {
+        assertThrows(
+            "Expected invalid allowed authenticator with cryptoObject to throw " +
+                "IllegalArgumentException",
+            java.lang.IllegalArgumentException::class.java
+        ) {
+            BiometricPromptData(TEST_CRYPTO_OBJECT)
+        }
+    }
+
+    @Test
+    fun construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
+        val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_STRONG
+
+        val biometricPromptData = BiometricPromptData(cryptoObject = null, expectedAuthenticator)
+
+        assertThat(biometricPromptData.cryptoObject).isNull()
+        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
+    }
+
+    @Test
+    fun construct_authenticatorNotAccepted_throwsIAE() {
+        assertThrows(
+            "Expected invalid allowed authenticator IllegalArgumentException",
+            java.lang.IllegalArgumentException::class.java
+        ) {
+            BiometricPromptData(null, allowedAuthenticators = Int.MIN_VALUE)
+        }
+    }
+
+    @Test
+    fun build_requiredParamsOnly_success() {
+        val expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+
+        val actualBiometricPromptData = BiometricPromptData.Builder().build()
+
+        assertThat(actualBiometricPromptData.allowedAuthenticators)
+            .isEqualTo(expectedAllowedAuthenticators)
+        assertThat(actualBiometricPromptData.cryptoObject).isNull()
+    }
+
+    @Test
+    fun build_setCryptoObjectWithStrongAuthenticatorOnly_success() {
+        val actualBiometricPromptData =
+            BiometricPromptData.Builder()
+                .setCryptoObject(TEST_CRYPTO_OBJECT)
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build()
+
+        assertThat(actualBiometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
+        assertThat(actualBiometricPromptData.allowedAuthenticators)
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+    }
+
+    @Test
+    fun build_setAllowedAuthenticator_success() {
+        val actualBiometricPromptData =
+            BiometricPromptData.Builder()
+                .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR)
+                .build()
+
+        assertThat(actualBiometricPromptData.allowedAuthenticators)
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+    }
+
+    @Test
+    fun build_setInvalidAllowedAuthenticator_success() {
+        assertThrows(
+            "Expected builder invalid allowed authenticator to throw " + "IllegalArgumentException",
+            java.lang.IllegalArgumentException::class.java
+        ) {
+            BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 34)
+    @Test
+    fun fromBundle_validAllowedAuthenticator_success() {
+        val inputBundle = Bundle()
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
+
+        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+        assertThat(actualBiometricPromptData).isNotNull()
+        assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+        assertThat(actualBiometricPromptData.cryptoObject).isNull()
+    }
+
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    fun fromBundle_validAllowedAuthenticatorAboveApi35_success() {
+        val expectedOpId = Integer.MIN_VALUE
+        val inputBundle = Bundle()
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
+        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
+
+        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+        assertThat(actualBiometricPromptData).isNotNull()
+        assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+        assertThat(actualBiometricPromptData.cryptoObject).isNotNull()
+        assertThat(actualBiometricPromptData.cryptoObject!!.hashCode()).isEqualTo(expectedOpId)
+    }
+
+    @Test
+    fun fromBundle_unrecognizedAllowedAuthenticator_success() {
+        val inputBundle = Bundle()
+        val unrecognizedAuthenticator = Integer.MAX_VALUE
+        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator)
+
+        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+        assertThat(actualBiometricPromptData).isNotNull()
+        assertThat(actualBiometricPromptData!!.allowedAuthenticators)
+            .isEqualTo(unrecognizedAuthenticator)
+    }
+
+    @Test
+    fun fromBundle_invalidBundleKey_nullBiometricPromptData() {
+        val expectedOpId = Integer.MIN_VALUE
+        val inputBundle = Bundle()
+        val unrecognizedAuthenticator = Integer.MAX_VALUE
+        inputBundle.putInt("invalid key", unrecognizedAuthenticator)
+        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
+
+        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
+
+        assertThat(actualBiometricPromptData).isNull()
+    }
+
+    @SdkSuppress(maxSdkVersion = 34)
+    @Test
+    fun toBundle_success() {
+        val testBiometricPromptData =
+            BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+
+        val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
+
+        assertThat(actualBundle).isNotNull()
+        assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS))
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID))
+            .isEqualTo(DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID)
+    }
+
+    @SdkSuppress(minSdkVersion = 35)
+    @Test
+    fun toBundle_api35AndAboveWithOpId_success() {
+        val testBiometricPromptData =
+            BiometricPromptData(TEST_CRYPTO_OBJECT, TEST_ALLOWED_AUTHENTICATOR)
+        val expectedOpId = TEST_CRYPTO_OBJECT.hashCode()
+
+        val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
+
+        assertThat(actualBundle).isNotNull()
+        assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS))
+            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
+        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)
+    }
+
+    private companion object {
+        private val TEST_CRYPTO_OBJECT = BiometricPrompt.CryptoObject(NullCipher())
+
+        private const val DEFAULT_BUNDLE_LONG_FOR_CRYPTO_ID = 0L
+
+        private const val TEST_ALLOWED_AUTHENTICATOR =
+            BiometricManager.Authenticators.BIOMETRIC_STRONG
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
index baaf9ee..1b9d081 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.BeforeClass
@@ -192,7 +193,8 @@
                         packageName,
                         PackageManager.GET_SIGNING_CERTIFICATES
                     )
-                signingInfo = packageInfo.signingInfo
+                assertNotNull(packageInfo.signingInfo)
+                signingInfo = packageInfo.signingInfo!!
             } catch (_: PackageManager.NameNotFoundException) {}
         }
     }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
index 5b332ac..2a3d871 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
@@ -18,15 +18,24 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
 import android.content.Intent;
+import android.content.pm.SigningInfo;
+import android.credentials.CredentialOption;
 import android.os.Build;
+import android.os.Bundle;
+import android.service.credentials.CallingAppInfo;
 
 import androidx.annotation.RequiresApi;
 import androidx.credentials.CreatePasswordResponse;
 import androidx.credentials.GetCredentialResponse;
 import androidx.credentials.PasswordCredential;
+import androidx.credentials.TestUtilsKt;
 import androidx.credentials.exceptions.CreateCredentialInterruptedException;
 import androidx.credentials.exceptions.GetCredentialInterruptedException;
+import androidx.credentials.provider.utils.EntryUtilsKt;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
@@ -34,6 +43,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Collections;
+
 @RequiresApi(34)
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -41,6 +53,244 @@
 public class PendingIntentHandlerJavaTest {
     private static final Intent BLANK_INTENT = new Intent();
 
+    private static final android.credentials.CredentialOption
+            GET_CREDENTIAL_OPTION = new CredentialOption.Builder(
+            "type", new Bundle(), new Bundle())
+            .build();
+
+    private static final android.service.credentials.GetCredentialRequest
+            GET_CREDENTIAL_REQUEST = new android.service.credentials.GetCredentialRequest(
+                    new CallingAppInfo(
+                            "package_name", new SigningInfo()), new ArrayList<>(
+                                    Collections.singleton(GET_CREDENTIAL_OPTION)));
+
+    private static final int BIOMETRIC_AUTHENTICATOR_TYPE = 1;
+
+    private static final int BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5;
+
+    private static final String BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error";
+
+    @Test
+    public void test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+        for (int jetpackResult :
+                AuthenticationResult.Companion
+                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(new AuthenticationResult(jetpackResult));
+            android.service.credentials.CreateCredentialRequest request =
+                    TestUtilsKt.setUpCreatePasswordRequest();
+            Intent intent = prepareIntentWithCreateRequest(
+                    request,
+                    biometricPromptResult);
+
+            ProviderCreateCredentialRequest retrievedRequest =
+                    PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
+
+            assertNotNull(request);
+            TestUtilsKt.equals(request, retrievedRequest);
+            assertNotNull(biometricPromptResult.getAuthenticationResult());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+                    .getAuthenticationType(), jetpackResult);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+        for (int jetpackResult :
+                AuthenticationResult.Companion
+                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(new AuthenticationResult(jetpackResult));
+            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                    biometricPromptResult);
+
+            ProviderGetCredentialRequest retrievedRequest =
+                    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+                    .getAuthenticationType(), jetpackResult);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_resultConverted() {
+        for (int frameworkResult :
+                AuthenticationResult.Companion
+                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(
+                            AuthenticationResult.Companion.createFrom$credentials_debug(
+                                    frameworkResult,
+                                    /*isFrameworkBiometricPrompt=*/true
+                            ));
+            android.service.credentials.CreateCredentialRequest request =
+                    TestUtilsKt.setUpCreatePasswordRequest();
+            int expectedResult =
+                    AuthenticationResult.Companion
+                            .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+                            .get(frameworkResult);
+            Intent intent = prepareIntentWithCreateRequest(
+                    request,
+                    biometricPromptResult);
+
+            ProviderCreateCredentialRequest retrievedRequest =
+                    PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
+
+            assertNotNull(request);
+            TestUtilsKt.equals(request, retrievedRequest);
+            assertNotNull(biometricPromptResult.getAuthenticationResult());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+                    .getAuthenticationType(), expectedResult);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_resultConverted() {
+        for (int frameworkResult :
+                AuthenticationResult.Companion
+                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(
+                            AuthenticationResult.Companion.createFrom$credentials_debug(
+                                    frameworkResult,
+                                    /*isFrameworkBiometricPrompt=*/true
+                            ));
+            int expectedResult =
+                    AuthenticationResult.Companion
+                            .getBiometricFrameworkToJetpackResultMap$credentials_debug()
+                            .get(frameworkResult);
+            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                    biometricPromptResult);
+
+            ProviderGetCredentialRequest retrievedRequest =
+                    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
+                    .getAuthenticationType(), expectedResult);
+        }
+    }
+
+
+    @Test
+    public void test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+        for (int jetpackError :
+                AuthenticationError.Companion
+                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(
+                            new AuthenticationError(
+                                    jetpackError,
+                                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+            android.service.credentials.CreateCredentialRequest request =
+                    TestUtilsKt.setUpCreatePasswordRequest();
+            Intent intent = prepareIntentWithCreateRequest(
+                    request, biometricPromptResult);
+
+            ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+                    .retrieveProviderCreateCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(request, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+                    .getErrorCode(), jetpackError);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+        for (int jetpackError :
+                AuthenticationError.Companion
+                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
+            BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+                    new AuthenticationError(
+                            jetpackError,
+                            BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                    biometricPromptResult);
+
+            ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
+                    .retrieveProviderGetCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+            assertEquals(
+                    retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+                            .getErrorCode(), jetpackError);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_errorConverted() {
+        for (int frameworkError :
+                AuthenticationError.Companion
+                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+            BiometricPromptResult biometricPromptResult =
+                    new BiometricPromptResult(
+                            AuthenticationError.Companion.createFrom$credentials_debug(
+                                    frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+                                    /*isFrameworkBiometricPrompt=*/true
+                            ));
+            android.service.credentials.CreateCredentialRequest request =
+                    TestUtilsKt.setUpCreatePasswordRequest();
+            int expectedErrorCode =
+                    AuthenticationError.Companion
+                            .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+                            .get(frameworkError);
+            Intent intent = prepareIntentWithCreateRequest(
+                    request, biometricPromptResult);
+
+            ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+                    .retrieveProviderCreateCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(request, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+                    .getErrorCode(), expectedErrorCode);
+        }
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedErr() {
+        for (int frameworkError :
+                AuthenticationError.Companion
+                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
+            BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+                    AuthenticationError.Companion.createFrom$credentials_debug(
+                            frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+                            /*isFrameworkBiometricPrompt=*/true
+                    ));
+            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                    biometricPromptResult);
+            int expectedErrorCode =
+                    AuthenticationError.Companion
+                            .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
+                            .get(frameworkError);
+
+            ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
+                    .retrieveProviderGetCredentialRequest(intent);
+
+            assertNotNull(retrievedRequest);
+            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
+            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
+            assertEquals(
+                    retrievedRequest.getBiometricPromptResult().getAuthenticationError()
+                            .getErrorCode(), expectedErrorCode);
+        }
+    }
+
     @Test
     public void test_setGetCreateCredentialException() {
         if (Build.VERSION.SDK_INT >= 34) {
@@ -157,6 +407,126 @@
     }
 
     @Test
+    public void test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
+        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+                new AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE));
+
+        android.service.credentials.CreateCredentialRequest request =
+                TestUtilsKt.setUpCreatePasswordRequest();
+
+        Intent intent = prepareIntentWithCreateRequest(request,
+                biometricPromptResult);
+
+        ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+                .retrieveProviderCreateCredentialRequest(intent);
+
+        assertNotNull(retrievedRequest);
+        TestUtilsKt.equals(request, retrievedRequest);
+        assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+    }
+
+    @Test
+    public void test_retrieveProviderCreateCredReqWithFailureBpAuth() {
+        BiometricPromptResult biometricPromptResult =
+                new BiometricPromptResult(
+                        new AuthenticationError(
+                                BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+                                BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+        android.service.credentials.CreateCredentialRequest request =
+                TestUtilsKt.setUpCreatePasswordRequest();
+        Intent intent = prepareIntentWithCreateRequest(
+                request, biometricPromptResult);
+
+        ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
+                .retrieveProviderCreateCredentialRequest(intent);
+
+        assertNotNull(retrievedRequest);
+        TestUtilsKt.equals(request, retrievedRequest);
+        assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
+        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+                new AuthenticationResult(
+                BIOMETRIC_AUTHENTICATOR_TYPE));
+        Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                biometricPromptResult);
+
+        ProviderGetCredentialRequest request = PendingIntentHandler
+                .retrieveProviderGetCredentialRequest(intent);
+
+        assertNotNull(request);
+        TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
+        assertEquals(biometricPromptResult, request.getBiometricPromptResult());
+    }
+
+    @Test
+    public void test_retrieveProviderGetCredReqWithFailingBpAuth() {
+        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
+                new AuthenticationError(
+                        BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+                        BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
+        Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
+                biometricPromptResult);
+
+        ProviderGetCredentialRequest request = PendingIntentHandler
+                .retrieveProviderGetCredentialRequest(intent);
+
+        assertNotNull(request);
+        TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
+        assertEquals(biometricPromptResult, request.getBiometricPromptResult());
+    }
+
+    private Intent prepareIntentWithGetRequest(
+            android.service.credentials.GetCredentialRequest request,
+            BiometricPromptResult biometricPromptResult
+    ) {
+        Intent intent = new Intent();
+        intent.putExtra(CredentialProviderService
+                        .EXTRA_GET_CREDENTIAL_REQUEST, request);
+        prepareIntentWithBiometricResult(intent, biometricPromptResult);
+        return intent;
+    }
+
+    private Intent prepareIntentWithCreateRequest(
+            android.service.credentials.CreateCredentialRequest request,
+            BiometricPromptResult biometricPromptResult) {
+        Intent intent = new Intent();
+        intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
+                request);
+        prepareIntentWithBiometricResult(intent, biometricPromptResult);
+        return intent;
+    }
+
+    private void prepareIntentWithBiometricResult(Intent intent,
+            BiometricPromptResult biometricPromptResult) {
+        String buildId = Build.ID;
+        if (biometricPromptResult.isSuccessful()) {
+            assertNotNull(biometricPromptResult.getAuthenticationResult());
+            String extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE;
+            if (EntryUtilsKt.requiresSlicePropertiesWorkaround()) {
+                extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK;
+            }
+            intent.putExtra(extraResultKey,
+                    biometricPromptResult.getAuthenticationResult().getAuthenticationType());
+        } else {
+            assertNotNull(biometricPromptResult.getAuthenticationError());
+            String extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR;
+            String extraErrorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE;
+            if (EntryUtilsKt.requiresSlicePropertiesWorkaround()) {
+                extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK;
+                extraErrorMessageKey = AuthenticationError
+                        .EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK;
+            }
+            intent.putExtra(extraErrorKey,
+                    biometricPromptResult.getAuthenticationError().getErrorCode());
+            intent.putExtra(extraErrorMessageKey,
+                    biometricPromptResult.getAuthenticationError().getErrorMsg());
+        }
+    }
+
+    @Test
     public void test_createCredentialCredentialResponse() {
         if (Build.VERSION.SDK_INT >= 34) {
             return;
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
index c9ed499..af32819 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
@@ -16,17 +16,27 @@
 package androidx.credentials.provider
 
 import android.content.Intent
+import android.content.pm.SigningInfo
+import android.credentials.CredentialOption
 import android.os.Build
+import android.os.Bundle
+import android.service.credentials.CallingAppInfo
+import android.service.credentials.CreateCredentialRequest
+import android.service.credentials.GetCredentialRequest
 import androidx.annotation.RequiresApi
 import androidx.credentials.CreatePasswordResponse
 import androidx.credentials.GetCredentialResponse
 import androidx.credentials.PasswordCredential
+import androidx.credentials.equals
 import androidx.credentials.exceptions.CreateCredentialInterruptedException
 import androidx.credentials.exceptions.GetCredentialInterruptedException
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
+import androidx.credentials.setUpCreatePasswordRequest
 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.Assert
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +45,238 @@
 @RequiresApi(34)
 @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
 class PendingIntentHandlerTest {
+    companion object {
+        private val GET_CREDENTIAL_OPTION =
+            CredentialOption.Builder("type", Bundle(), Bundle()).build()
+
+        private val GET_CREDENTIAL_REQUEST =
+            GetCredentialRequest(
+                CallingAppInfo("package_name", SigningInfo()),
+                ArrayList(setOf(GET_CREDENTIAL_OPTION))
+            )
+
+        private const val BIOMETRIC_AUTHENTICATOR_TYPE = 1
+
+        private const val BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5
+
+        private const val BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error"
+
+        private const val FRAMEWORK_EXPECTED_CONSTANT_ERROR_CODE =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
+
+        private const val FRAMEWORK_EXPECTED_CONSTANT_ERROR_MESSAGE =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
+
+        private const val FRAMEWORK_EXPECTED_CONSTANT_AUTH_RESULT =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_RESULT"
+    }
+
+    @Test
+    fun test_constantsMatchFrameworkExpectations_success() {
+        assertThat(AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE)
+            .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_AUTH_RESULT)
+        assertThat(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR)
+            .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_ERROR_CODE)
+        assertThat(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE)
+            .isEqualTo(FRAMEWORK_EXPECTED_CONSTANT_ERROR_MESSAGE)
+    }
+
+    @Test
+    fun test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+        for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
+            val biometricPromptResult = BiometricPromptResult(AuthenticationResult(jetpackResult))
+            val request = setUpCreatePasswordRequest()
+            val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+            val retrievedRequest =
+                PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+            Assert.assertNotNull(request)
+            equals(request, retrievedRequest!!)
+            Assert.assertNotNull(biometricPromptResult.authenticationResult)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationResult!!.authenticationType,
+                jetpackResult
+            )
+        }
+    }
+
+    @Test
+    fun test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
+        for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
+            val biometricPromptResult = BiometricPromptResult(AuthenticationResult(jetpackResult))
+            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+            val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+            Assert.assertNotNull(request)
+            equals(GET_CREDENTIAL_REQUEST, request!!)
+            Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+            Assert.assertEquals(
+                request.biometricPromptResult!!.authenticationResult!!.authenticationType,
+                jetpackResult
+            )
+        }
+    }
+
+    // While possible to test non-conversion logic, that would equate functionally to the normal
+    // jetpack tests as there is no validation.
+    @Test
+    fun test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
+        for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationResult.createFrom(
+                        uiAuthenticationType = frameworkResult,
+                        isFrameworkBiometricPrompt = true
+                    )
+                )
+            val request = setUpCreatePasswordRequest()
+            val expectedResult =
+                AuthenticationResult.biometricFrameworkToJetpackResultMap[frameworkResult]
+            val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+            val retrievedRequest =
+                PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+            Assert.assertNotNull(request)
+            equals(request, retrievedRequest!!)
+            Assert.assertNotNull(biometricPromptResult.authenticationResult)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationResult!!.authenticationType,
+                expectedResult
+            )
+        }
+    }
+
+    // While possible to test non-conversion logic, that would equate functionally to the normal
+    // jetpack tests as there is no validation.
+    @Test
+    fun test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
+        for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationResult.createFrom(
+                        uiAuthenticationType = frameworkResult,
+                        isFrameworkBiometricPrompt = true
+                    )
+                )
+            val expectedResult =
+                AuthenticationResult.biometricFrameworkToJetpackResultMap[frameworkResult]
+            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+            val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+            Assert.assertNotNull(request)
+            equals(GET_CREDENTIAL_REQUEST, request!!)
+            Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+            Assert.assertEquals(
+                request.biometricPromptResult!!.authenticationResult!!.authenticationType,
+                expectedResult
+            )
+        }
+    }
+
+    @Test
+    fun test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+        for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationError(jetpackError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG)
+                )
+            val request = setUpCreatePasswordRequest()
+            val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+            val retrievedRequest =
+                PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+            Assert.assertNotNull(retrievedRequest)
+            equals(request, retrievedRequest!!)
+            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+                jetpackError
+            )
+        }
+    }
+
+    @Test
+    fun test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
+        for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationError(jetpackError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG)
+                )
+            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+            val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+            Assert.assertNotNull(retrievedRequest)
+            equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
+            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+                jetpackError
+            )
+        }
+    }
+
+    @Test
+    fun test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
+        for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationError.createFrom(
+                        uiErrorCode = frameworkError,
+                        uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+                        isFrameworkBiometricPrompt = true
+                    )
+                )
+            val expectedErrorCode =
+                AuthenticationError.biometricFrameworkToJetpackErrorMap[frameworkError]
+            val request = setUpCreatePasswordRequest()
+            val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+            val retrievedRequest =
+                PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+            Assert.assertNotNull(retrievedRequest)
+            equals(request, retrievedRequest!!)
+            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+                expectedErrorCode
+            )
+        }
+    }
+
+    @Test
+    fun test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
+        for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
+            val biometricPromptResult =
+                BiometricPromptResult(
+                    AuthenticationError.createFrom(
+                        uiErrorCode = frameworkError,
+                        uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
+                        isFrameworkBiometricPrompt = true
+                    )
+                )
+            val expectedErrorCode =
+                AuthenticationError.biometricFrameworkToJetpackErrorMap[frameworkError]
+            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+            val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+            Assert.assertNotNull(retrievedRequest)
+            equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
+            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
+            Assert.assertEquals(
+                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
+                expectedErrorCode
+            )
+        }
+    }
+
     @Test
     fun test_createCredentialException() {
         if (Build.VERSION.SDK_INT >= 34) {
@@ -51,7 +293,7 @@
         assertThat(finalException).isEqualTo(initialException)
     }
 
-    @Test()
+    @Test
     fun test_createCredentialException_throwsWhenEmptyIntent() {
         if (Build.VERSION.SDK_INT >= 34) {
             return
@@ -62,6 +304,127 @@
     }
 
     @Test
+    fun test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
+        val biometricPromptResult =
+            BiometricPromptResult(AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE))
+        val request = setUpCreatePasswordRequest()
+        val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+        val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+        Assert.assertNotNull(request)
+        equals(request, retrievedRequest!!)
+        Assert.assertNotNull(biometricPromptResult.authenticationResult)
+    }
+
+    @Test
+    fun test_retrieveProviderCreateCredReqWithFailureBpAuth() {
+        val biometricPromptResult =
+            BiometricPromptResult(
+                AuthenticationError(
+                    BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG
+                )
+            )
+        val request = setUpCreatePasswordRequest()
+        val intent = prepareIntentWithCreateRequest(request, biometricPromptResult)
+
+        val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+
+        Assert.assertNotNull(retrievedRequest)
+        equals(request, retrievedRequest!!)
+        Assert.assertEquals(biometricPromptResult, retrievedRequest.biometricPromptResult)
+    }
+
+    @Test
+    fun test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
+        val biometricPromptResult =
+            BiometricPromptResult(AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE))
+        val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+        val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+        Assert.assertNotNull(request)
+        equals(GET_CREDENTIAL_REQUEST, request!!)
+        Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+    }
+
+    @Test
+    fun test_retrieveProviderGetCredReqWithFailingBpAuth() {
+        val biometricPromptResult =
+            BiometricPromptResult(
+                AuthenticationError(
+                    BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
+                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG
+                )
+            )
+        val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
+
+        val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+
+        Assert.assertNotNull(request)
+        equals(GET_CREDENTIAL_REQUEST, request!!)
+        Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
+    }
+
+    private fun prepareIntentWithGetRequest(
+        request: GetCredentialRequest,
+        biometricPromptResult: BiometricPromptResult
+    ): Intent {
+        val intent = Intent()
+        intent.putExtra(
+            android.service.credentials.CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+            request
+        )
+        prepareIntentWithBiometricResult(intent, biometricPromptResult)
+        return intent
+    }
+
+    private fun prepareIntentWithCreateRequest(
+        request: CreateCredentialRequest,
+        biometricPromptResult: BiometricPromptResult
+    ): Intent {
+        val intent = Intent()
+        intent.putExtra(
+            android.service.credentials.CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
+            request
+        )
+        prepareIntentWithBiometricResult(intent, biometricPromptResult)
+        return intent
+    }
+
+    private fun prepareIntentWithBiometricResult(
+        intent: Intent,
+        biometricPromptResult: BiometricPromptResult
+    ) {
+        if (biometricPromptResult.isSuccessful) {
+            Assert.assertNotNull(biometricPromptResult.authenticationResult)
+            var extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE
+            if (requiresSlicePropertiesWorkaround()) {
+                extraResultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK
+            }
+            intent.putExtra(
+                extraResultKey,
+                biometricPromptResult.authenticationResult!!.authenticationType
+            )
+        } else {
+            Assert.assertNotNull(biometricPromptResult.authenticationError)
+            var extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR
+            var extraErrorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE
+            if (requiresSlicePropertiesWorkaround()) {
+                extraErrorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK
+                extraErrorMessageKey =
+                    AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK
+            }
+            intent.putExtra(extraErrorKey, biometricPromptResult.authenticationError!!.errorCode)
+            intent.putExtra(
+                extraErrorMessageKey,
+                biometricPromptResult.authenticationError!!.errorMsg
+            )
+        }
+    }
+
+    @Test
     fun test_credentialException() {
         if (Build.VERSION.SDK_INT >= 34) {
             return
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
index 3f86607..22d51e6 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertThrows;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 
@@ -80,9 +79,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void fromSlice_success() {
         Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
-        Slice slice = Action.toSlice(originalAction);
+        android.app.slice.Slice slice = Action.toSlice(originalAction);
 
         Action fromSlice = Action.fromSlice(slice);
 
@@ -94,9 +94,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromAction_success() {
         Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
-        Slice slice = Action.toSlice(originalAction);
+        android.app.slice.Slice slice = Action.toSlice(originalAction);
 
         Action action = Action.fromAction(new android.service.credentials.Action(slice));
 
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
index d628df5..d5a75c5 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertThrows;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 
@@ -75,9 +74,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void fromSlice_success() {
         AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
-        Slice slice = AuthenticationAction.toSlice(originalAction);
+        android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
 
         AuthenticationAction fromSlice = AuthenticationAction.fromSlice(slice);
 
@@ -87,9 +87,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromAction_success() {
         AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
-        Slice slice = AuthenticationAction.toSlice(originalAction);
+        android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
         assertNotNull(slice);
 
         AuthenticationAction action = AuthenticationAction.fromAction(
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
index 0de5cc2..83a2d1d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
@@ -23,12 +23,17 @@
 import static org.junit.Assert.assertThrows;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
+import android.os.Build;
 
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
+import androidx.credentials.provider.BiometricPromptData;
 import androidx.credentials.provider.CreateEntry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -40,8 +45,10 @@
 
 import java.time.Instant;
 
+import javax.crypto.NullCipher;
+
 @RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 public class CreateEntryJavaTest {
     private static final CharSequence ACCOUNT_NAME = "account_name";
@@ -73,7 +80,17 @@
     }
 
     @Test
-    public void constructor_allParameters_success() {
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+    public void constructor_allParametersAboveApiO_success() {
+        CreateEntry entry = constructEntryWithAllParams();
+
+        assertNotNull(entry);
+        assertEntryWithAllParams(entry);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+    public void constructor_allParametersApiOAndBelow_success() {
         CreateEntry entry = constructEntryWithAllParams();
 
         assertNotNull(entry);
@@ -128,9 +145,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromCreateEntry_allParams_success() {
         CreateEntry originalEntry = constructEntryWithAllParams();
-        Slice slice = CreateEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = CreateEntry.toSlice(originalEntry);
         assertNotNull(slice);
 
         CreateEntry entry = CreateEntry.fromCreateEntry(
@@ -147,18 +165,22 @@
     private void assertEntryWithRequiredParams(CreateEntry entry) {
         assertThat(ACCOUNT_NAME.equals(entry.getAccountName()));
         assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
 
     private CreateEntry constructEntryWithAllParams() {
-        return new CreateEntry.Builder(
+        CreateEntry.Builder testBuilder = new CreateEntry.Builder(
                 ACCOUNT_NAME,
                 mPendingIntent)
                 .setIcon(ICON)
                 .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
                 .setPasswordCredentialCount(PASSWORD_COUNT)
                 .setPublicKeyCredentialCount(PUBLIC_KEY_CREDENTIAL_COUNT)
-                .setTotalCredentialCount(TOTAL_COUNT)
-                .build();
+                .setTotalCredentialCount(TOTAL_COUNT);
+        if (BuildCompat.isAtLeastV()) {
+            testBuilder.setBiometricPromptData(testBiometricPromptData());
+        }
+        return testBuilder.build();
     }
 
     private void assertEntryWithAllParams(CreateEntry entry) {
@@ -169,5 +191,25 @@
         assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount());
         assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount());
         assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount());
+        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+            assertAboveApiV(entry);
+        } else {
+            assertThat(entry.getBiometricPromptData()).isNull();
+        }
+    }
+
+    private static void assertAboveApiV(CreateEntry entry) {
+        if (BuildCompat.isAtLeastV()) {
+            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+                    testBiometricPromptData().getAllowedAuthenticators());
+        }
+    }
+
+    @RequiresApi(35)
+    private static BiometricPromptData testBiometricPromptData() {
+        return new BiometricPromptData.Builder()
+                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build();
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
index 606a8fa..4c6810e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
@@ -20,6 +20,12 @@
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.drawable.Icon
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
+import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.CreateEntry
 import androidx.credentials.provider.CreateEntry.Companion.fromCreateEntry
 import androidx.credentials.provider.CreateEntry.Companion.fromSlice
@@ -28,8 +34,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import java.time.Instant
+import javax.crypto.NullCipher
 import org.junit.Assert
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
@@ -38,7 +45,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 class CreateEntryTest {
     private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -74,7 +81,8 @@
     }
 
     @Test
-    fun constructor_allParameters_success() {
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+    fun constructor_allParametersAboveApiO_success() {
         val entry = constructEntryWithAllParams()
 
         assertNotNull(entry)
@@ -139,33 +147,55 @@
     }
 
     private fun assertEntryWithRequiredParams(entry: CreateEntry) {
-        Truth.assertThat(ACCOUNT_NAME == entry.accountName)
-        Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+        assertThat(ACCOUNT_NAME == entry.accountName)
+        assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     private fun constructEntryWithAllParams(): CreateEntry {
-        return CreateEntry(
-            ACCOUNT_NAME,
-            mPendingIntent,
-            DESCRIPTION,
-            Instant.ofEpochMilli(LAST_USED_TIME),
-            ICON,
-            PASSWORD_COUNT,
-            PUBLIC_KEY_CREDENTIAL_COUNT,
-            TOTAL_COUNT,
-            AUTO_SELECT_BIT
-        )
+        if (BuildCompat.isAtLeastV()) {
+            return CreateEntry(
+                ACCOUNT_NAME,
+                mPendingIntent,
+                DESCRIPTION,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                PASSWORD_COUNT,
+                PUBLIC_KEY_CREDENTIAL_COUNT,
+                TOTAL_COUNT,
+                AUTO_SELECT_BIT,
+                testBiometricPromptData()
+            )
+        } else {
+            return CreateEntry(
+                ACCOUNT_NAME,
+                mPendingIntent,
+                DESCRIPTION,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                PASSWORD_COUNT,
+                PUBLIC_KEY_CREDENTIAL_COUNT,
+                TOTAL_COUNT,
+                AUTO_SELECT_BIT
+            )
+        }
     }
 
     private fun assertEntryWithAllParams(entry: CreateEntry) {
-        Truth.assertThat(ACCOUNT_NAME).isEqualTo(entry.accountName)
-        Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
-        Truth.assertThat(ICON).isEqualTo(entry.icon)
-        Truth.assertThat(LAST_USED_TIME).isEqualTo(entry.lastUsedTime?.toEpochMilli())
-        Truth.assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount())
-        Truth.assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount())
-        Truth.assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount())
-        Truth.assertThat(AUTO_SELECT_BIT).isTrue()
+        assertThat(ACCOUNT_NAME).isEqualTo(entry.accountName)
+        assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
+        assertThat(ICON).isEqualTo(entry.icon)
+        assertThat(LAST_USED_TIME).isEqualTo(entry.lastUsedTime?.toEpochMilli())
+        assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount())
+        assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount())
+        assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount())
+        assertThat(AUTO_SELECT_BIT).isTrue()
+        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+            assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+                .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+        } else {
+            assertThat(entry.biometricPromptData).isNull()
+        }
     }
 
     companion object {
@@ -178,5 +208,13 @@
         private const val LAST_USED_TIME = 10L
         private val ICON =
             Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+
+        @RequiresApi(35)
+        private fun testBiometricPromptData(): BiometricPromptData {
+            return BiometricPromptData.Builder()
+                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build()
+        }
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
index cd6ab67..d93cd88 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
@@ -25,7 +25,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -33,10 +32,15 @@
 import android.os.Bundle;
 import android.service.credentials.CredentialEntry;
 
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
 import androidx.credentials.R;
 import androidx.credentials.TestUtilsKt;
 import androidx.credentials.provider.BeginGetCredentialOption;
 import androidx.credentials.provider.BeginGetCustomCredentialOption;
+import androidx.credentials.provider.BiometricPromptData;
 import androidx.credentials.provider.CustomCredentialEntry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -47,8 +51,11 @@
 import org.junit.runner.RunWith;
 
 import java.time.Instant;
+
+import javax.crypto.NullCipher;
+
 @RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 public class CustomCredentialEntryJavaTest {
     private static final CharSequence TITLE = "title";
@@ -149,6 +156,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 28)
     public void builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
         CustomCredentialEntry entry = constructEntryWithRequiredParams();
 
@@ -180,9 +188,11 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void fromSlice_requiredParams_success() {
         CustomCredentialEntry originalEntry = constructEntryWithRequiredParams();
-        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = CustomCredentialEntry
+                .toSlice(originalEntry);
         CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(
                 slice);
         assertNotNull(entry);
@@ -190,18 +200,21 @@
     }
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void fromSlice_allParams_success() {
         CustomCredentialEntry originalEntry = constructEntryWithAllParams();
-        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = CustomCredentialEntry
+                .toSlice(originalEntry);
         CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(slice);
         assertNotNull(entry);
         assertEntryWithAllParamsFromSlice(entry);
     }
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromCredentialEntry_allParams_success() {
         CustomCredentialEntry originalEntry = constructEntryWithAllParams();
-        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(originalEntry);
         assertNotNull(slice);
         CustomCredentialEntry entry = CustomCredentialEntry.fromCredentialEntry(
                 new CredentialEntry("id", slice));
@@ -220,11 +233,12 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
         CustomCredentialEntry entry = new CustomCredentialEntry
                 .Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption).build();
 
-        Slice slice = CustomCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
 
         assertNotNull(slice);
 
@@ -237,12 +251,13 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_customIconSetFromSlice_returnsTrue() {
         CustomCredentialEntry entry = new CustomCredentialEntry
                 .Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption)
                 .setIcon(ICON).build();
 
-        Slice slice = CustomCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
 
         assertNotNull(slice);
 
@@ -291,7 +306,7 @@
         ).build();
     }
     private CustomCredentialEntry constructEntryWithAllParams() {
-        return new CustomCredentialEntry.Builder(
+        CustomCredentialEntry.Builder testBuilder = new CustomCredentialEntry.Builder(
                 mContext,
                 TYPE,
                 TITLE,
@@ -302,8 +317,12 @@
                 .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
                 .setTypeDisplayName(TYPE_DISPLAY_NAME)
                 .setEntryGroupId(ENTRY_GROUP_ID)
-                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
-                .build();
+                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
+
+        if (BuildCompat.isAtLeastV()) {
+            testBuilder.setBiometricPromptData(testBiometricPromptData());
+        }
+        return testBuilder.build();
     }
     private void assertEntryWithRequiredParams(CustomCredentialEntry entry) {
         assertThat(TITLE.equals(entry.getTitle()));
@@ -313,6 +332,7 @@
         assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT);
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
     private void assertEntryWithRequiredParamsFromSlice(CustomCredentialEntry entry) {
         assertThat(TITLE.equals(entry.getTitle()));
@@ -322,6 +342,7 @@
         assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT);
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
     private void assertEntryWithAllParams(CustomCredentialEntry entry) {
         assertThat(TITLE.equals(entry.getTitle()));
@@ -338,6 +359,12 @@
         assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 SINGLE_PROVIDER_ICON_BIT);
+        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+                    testBiometricPromptData().getAllowedAuthenticators());
+        } else {
+            assertThat(entry.getBiometricPromptData()).isNull();
+        }
     }
     private void assertEntryWithAllParamsFromSlice(CustomCredentialEntry entry) {
         assertThat(TITLE.equals(entry.getTitle()));
@@ -353,5 +380,19 @@
         assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 SINGLE_PROVIDER_ICON_BIT);
+        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+                    testBiometricPromptData().getAllowedAuthenticators());
+        } else {
+            assertThat(entry.getBiometricPromptData()).isNull();
+        }
+    }
+
+    @RequiresApi(35)
+    private static BiometricPromptData testBiometricPromptData() {
+        return new BiometricPromptData.Builder()
+            .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+            .build();
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
index 6669355..70aa96d 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
@@ -22,11 +22,16 @@
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
 import androidx.credentials.CredentialOption
 import androidx.credentials.R
 import androidx.credentials.equals
 import androidx.credentials.provider.BeginGetCredentialOption
 import androidx.credentials.provider.BeginGetCustomCredentialOption
+import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.CustomCredentialEntry
 import androidx.credentials.provider.CustomCredentialEntry.Companion.fromCredentialEntry
 import androidx.credentials.provider.CustomCredentialEntry.Companion.fromSlice
@@ -37,6 +42,7 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
+import javax.crypto.NullCipher
 import org.junit.Assert
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertThrows
@@ -44,7 +50,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 class CustomCredentialEntryTest {
     private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -53,7 +59,6 @@
         PendingIntent.getActivity(mContext, 0, mIntent, PendingIntent.FLAG_IMMUTABLE)
 
     @Test
-    @SdkSuppress(minSdkVersion = 28)
     fun constructor_requiredParams_success() {
         val entry = constructEntryWithRequiredParams()
         assertNotNull(entry)
@@ -100,7 +105,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 23)
+    @SdkSuppress(minSdkVersion = 28)
     fun constructor_nullIcon_defaultIconSet() {
         val entry = constructEntryWithRequiredParams()
         assertThat(
@@ -183,6 +188,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 28)
     fun builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
         val entry =
             CustomCredentialEntry.Builder(mContext, TYPE, TITLE, mPendingIntent, BEGIN_OPTION)
@@ -201,6 +207,7 @@
         assertThat(entry.entryGroupId).isEqualTo(TITLE)
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     @Test
@@ -318,19 +325,36 @@
     }
 
     private fun constructEntryWithAllParams(): CustomCredentialEntry {
-        return CustomCredentialEntry(
-            mContext,
-            TITLE,
-            mPendingIntent,
-            BEGIN_OPTION,
-            SUBTITLE,
-            TYPE_DISPLAY_NAME,
-            Instant.ofEpochMilli(LAST_USED_TIME),
-            ICON,
-            IS_AUTO_SELECT_ALLOWED,
-            ENTRY_GROUP_ID,
-            SINGLE_PROVIDER_ICON_BIT
-        )
+        return if (BuildCompat.isAtLeastV()) {
+            CustomCredentialEntry(
+                mContext,
+                TITLE,
+                mPendingIntent,
+                BEGIN_OPTION,
+                SUBTITLE,
+                TYPE_DISPLAY_NAME,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                ENTRY_GROUP_ID,
+                SINGLE_PROVIDER_ICON_BIT,
+                testBiometricPromptData()
+            )
+        } else {
+            CustomCredentialEntry(
+                mContext,
+                TITLE,
+                mPendingIntent,
+                BEGIN_OPTION,
+                SUBTITLE,
+                TYPE_DISPLAY_NAME,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                ENTRY_GROUP_ID,
+                SINGLE_PROVIDER_ICON_BIT
+            )
+        }
     }
 
     private fun assertEntryWithAllParams(entry: CustomCredentialEntry) {
@@ -344,6 +368,12 @@
         assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
         assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
         assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
+        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+            assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+                .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+        } else {
+            assertThat(entry.biometricPromptData).isNull()
+        }
     }
 
     private fun assertEntryWithAllParamsFromSlice(entry: CustomCredentialEntry) {
@@ -358,6 +388,12 @@
         assertThat(BEGIN_OPTION.type).isEqualTo(entry.type)
         assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
         assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
+        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+            assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+                .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+        } else {
+            assertThat(entry.biometricPromptData).isNull()
+        }
     }
 
     private fun assertEntryWithRequiredParams(entry: CustomCredentialEntry) {
@@ -368,6 +404,7 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.entryGroupId).isEqualTo(TITLE)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     private fun assertEntryWithRequiredParamsFromSlice(entry: CustomCredentialEntry) {
@@ -377,6 +414,7 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.entryGroupId).isEqualTo(TITLE)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     companion object {
@@ -393,5 +431,13 @@
         private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
         private const val SINGLE_PROVIDER_ICON_BIT = true
         private const val ENTRY_GROUP_ID = "entryGroupId"
+
+        @RequiresApi(35)
+        private fun testBiometricPromptData(): BiometricPromptData {
+            return BiometricPromptData.Builder()
+                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build()
+        }
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
index 639f2ad..48ab2a9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
@@ -25,7 +25,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -33,10 +32,15 @@
 import android.os.Bundle;
 import android.service.credentials.CredentialEntry;
 
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
 import androidx.credentials.PasswordCredential;
 import androidx.credentials.R;
 import androidx.credentials.TestUtilsKt;
 import androidx.credentials.provider.BeginGetPasswordOption;
+import androidx.credentials.provider.BiometricPromptData;
 import androidx.credentials.provider.PasswordCredentialEntry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -48,8 +52,11 @@
 
 import java.time.Instant;
 import java.util.HashSet;
+
+import javax.crypto.NullCipher;
+
 @RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 public class PasswordCredentialEntryJavaTest {
     private static final CharSequence USERNAME = "title";
@@ -95,6 +102,7 @@
 
     @SdkSuppress(minSdkVersion = 28)
     @Test
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_customIconSetFromSlice_returnsFalse() {
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                 mContext,
@@ -103,7 +111,7 @@
                 mBeginGetPasswordOption
         ).setIcon(ICON).build();
 
-        Slice slice = PasswordCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
 
         assertNotNull(slice);
 
@@ -117,6 +125,7 @@
 
     @SdkSuppress(minSdkVersion = 28)
     @Test
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                 mContext,
@@ -125,7 +134,7 @@
                 mBeginGetPasswordOption
         ).build();
 
-        Slice slice = PasswordCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
         assertNotNull(slice);
         PasswordCredentialEntry entryFromSlice = PasswordCredentialEntry
                 .fromSlice(slice);
@@ -223,16 +232,20 @@
     @Test
     public void build_isAutoSelectAllowedDefault_false() {
         PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
         assertFalse(entry.isAutoSelectAllowed());
     }
     @Test
     public void constructor_defaultAffiliatedDomain() {
         PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
+
         assertThat(entry.getAffiliatedDomain()).isNull();
     }
+
     @Test
     public void constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
         String expectedAffiliatedDomain = "non-empty";
+
         PasswordCredentialEntry entryWithAffiliatedDomain = new PasswordCredentialEntry(
                 mContext,
                 USERNAME,
@@ -245,39 +258,47 @@
                 expectedAffiliatedDomain,
                 false
         );
+
         assertThat(entryWithAffiliatedDomain.getAffiliatedDomain())
                 .isEqualTo(expectedAffiliatedDomain);
     }
     @Test
+    @SdkSuppress(minSdkVersion = 34)
     public void builder_constructDefault_containsOnlyDefaultValuesForSettableParameters() {
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
                 mPendingIntent, mBeginGetPasswordOption).build();
+
         assertThat(entry.getAffiliatedDomain()).isNull();
         assertThat(entry.getDisplayName()).isNull();
         assertThat(entry.getLastUsedTime()).isNull();
         assertThat(entry.isAutoSelectAllowed()).isFalse();
         assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
     @Test
     public void builder_setAffiliatedDomainNull_retrieveNullAffiliatedDomain() {
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
                 mPendingIntent, mBeginGetPasswordOption).setAffiliatedDomain(null).build();
+
         assertThat(entry.getAffiliatedDomain()).isNull();
     }
     @Test
     public void builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
         String expectedAffiliatedDomain = "affiliated-domain";
+
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                 mContext,
                 USERNAME,
                 mPendingIntent,
                 mBeginGetPasswordOption
         ).setAffiliatedDomain(expectedAffiliatedDomain).build();
+
         assertThat(entry.getAffiliatedDomain()).isEqualTo(expectedAffiliatedDomain);
     }
     @Test
     public void builder_setPreferredDefaultIconBit_retrieveSetIconBit() {
         boolean expectedPreferredDefaultIconBit = SINGLE_PROVIDER_ICON_BIT;
+
         PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                 mContext,
                 USERNAME,
@@ -285,6 +306,7 @@
                 mBeginGetPasswordOption
         ).setDefaultIconPreferredAsSingleProvider(expectedPreferredDefaultIconBit)
                 .build();
+
         assertThat(entry.isDefaultIconPreferredAsSingleProvider())
                 .isEqualTo(expectedPreferredDefaultIconBit);
     }
@@ -323,20 +345,31 @@
                 mPendingIntent,
                 mBeginGetPasswordOption).build();
     }
+
     private PasswordCredentialEntry constructEntryWithAllParams() {
-        return new PasswordCredentialEntry.Builder(
-                mContext,
-                USERNAME,
-                mPendingIntent,
-                mBeginGetPasswordOption)
-                .setDisplayName(DISPLAYNAME)
-                .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
-                .setIcon(ICON)
-                .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
-                .setAffiliatedDomain(AFFILIATED_DOMAIN)
-                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
-                .build();
+        if (BuildCompat.isAtLeastV()) {
+            return new PasswordCredentialEntry.Builder(
+                    mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
+                    .setDisplayName(DISPLAYNAME)
+                    .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+                    .setIcon(ICON)
+                    .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+                    .setAffiliatedDomain(AFFILIATED_DOMAIN)
+                    .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
+                    .setBiometricPromptData(testBiometricPromptData()).build();
+        } else {
+            return new PasswordCredentialEntry.Builder(
+                    mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
+                    .setDisplayName(DISPLAYNAME)
+                    .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
+                    .setIcon(ICON)
+                    .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
+                    .setAffiliatedDomain(AFFILIATED_DOMAIN)
+                    .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
+                    .build();
+        }
     }
+
     private void assertEntryWithRequiredParamsOnly(PasswordCredentialEntry entry,
             Boolean assertOptionIdOnly) {
         assertThat(USERNAME.equals(entry.getUsername()));
@@ -346,6 +379,7 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT);
         assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
     private void assertEntryWithAllParams(PasswordCredentialEntry entry) {
         assertThat(USERNAME.equals(entry.getUsername()));
@@ -360,5 +394,19 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                 SINGLE_PROVIDER_ICON_BIT);
         assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+                    testBiometricPromptData().getAllowedAuthenticators());
+        } else {
+            assertThat(entry.getBiometricPromptData()).isNull();
+        }
+    }
+
+    @RequiresApi(35)
+    private static BiometricPromptData testBiometricPromptData() {
+        return new BiometricPromptData.Builder()
+                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build();
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
index 83b0853..bd72745 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
@@ -22,11 +22,16 @@
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
 import androidx.credentials.CredentialOption
 import androidx.credentials.PasswordCredential
 import androidx.credentials.R
 import androidx.credentials.equals
 import androidx.credentials.provider.BeginGetPasswordOption
+import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.PasswordCredentialEntry
 import androidx.credentials.provider.PasswordCredentialEntry.Companion.fromSlice
 import androidx.test.core.app.ApplicationProvider
@@ -35,6 +40,7 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
+import javax.crypto.NullCipher
 import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertNotNull
 import org.junit.Assert
@@ -43,7 +49,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 class PasswordCredentialEntryTest {
     private val mContext = ApplicationProvider.getApplicationContext<Context>()
@@ -182,12 +188,21 @@
     @Test
     fun constructor_defaultAffiliatedDomain() {
         val defaultEntry = constructEntryWithRequiredParamsOnly()
+
         assertThat(defaultEntry.affiliatedDomain).isNull()
     }
 
     @Test
+    fun constructor_defaultBiometricPromptData() {
+        val defaultEntry = constructEntryWithRequiredParamsOnly()
+
+        assertThat(defaultEntry.biometricPromptData).isNull()
+    }
+
+    @Test
     fun constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
         val expectedAffiliatedDomain = "non-empty"
+
         val entryWithAffiliationType =
             PasswordCredentialEntry(
                 mContext,
@@ -199,6 +214,7 @@
                 ICON,
                 affiliatedDomain = expectedAffiliatedDomain
             )
+
         assertThat(entryWithAffiliationType.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
     }
 
@@ -216,6 +232,7 @@
                 ICON,
                 isDefaultIconPreferredAsSingleProvider = expectedPreferredDefaultIconBit
             )
+
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(expectedPreferredDefaultIconBit)
     }
@@ -223,6 +240,7 @@
     @Test
     fun constructor_preferredIconBitNotProvided_retrieveDefaultPreferredIconBit() {
         val entry = PasswordCredentialEntry(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
+
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
     }
@@ -252,6 +270,7 @@
         assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
         assertThat(entry.affiliatedDomain).isNull()
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     @Test
@@ -260,16 +279,19 @@
             PasswordCredentialEntry.Builder(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
                 .setAffiliatedDomain(null)
                 .build()
+
         assertThat(entry.affiliatedDomain).isNull()
     }
 
     @Test
     fun builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
         val expectedAffiliatedDomain = "name"
+
         val entry =
             PasswordCredentialEntry.Builder(mContext, USERNAME, mPendingIntent, BEGIN_OPTION)
                 .setAffiliatedDomain(expectedAffiliatedDomain)
                 .build()
+
         assertThat(entry.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
     }
 
@@ -304,18 +326,34 @@
     }
 
     private fun constructEntryWithAllParams(): PasswordCredentialEntry {
-        return PasswordCredentialEntry(
-            mContext,
-            USERNAME,
-            mPendingIntent,
-            BEGIN_OPTION,
-            DISPLAYNAME,
-            LAST_USED_TIME,
-            ICON,
-            IS_AUTO_SELECT_ALLOWED,
-            AFFILIATED_DOMAIN,
-            SINGLE_PROVIDER_ICON_BIT
-        )
+        return if (BuildCompat.isAtLeastV()) {
+            PasswordCredentialEntry(
+                mContext,
+                USERNAME,
+                mPendingIntent,
+                BEGIN_OPTION,
+                DISPLAYNAME,
+                LAST_USED_TIME,
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                AFFILIATED_DOMAIN,
+                SINGLE_PROVIDER_ICON_BIT,
+                testBiometricPromptData(),
+            )
+        } else {
+            PasswordCredentialEntry(
+                mContext,
+                USERNAME,
+                mPendingIntent,
+                BEGIN_OPTION,
+                DISPLAYNAME,
+                LAST_USED_TIME,
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                AFFILIATED_DOMAIN,
+                SINGLE_PROVIDER_ICON_BIT,
+            )
+        }
     }
 
     private fun assertEntryWithRequiredParamsOnly(entry: PasswordCredentialEntry) {
@@ -325,6 +363,7 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider)
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     private fun assertEntryWithAllParams(entry: PasswordCredentialEntry) {
@@ -341,6 +380,13 @@
         assertThat(entry.affiliatedDomain).isEqualTo(AFFILIATED_DOMAIN)
         assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        if (BuildCompat.isAtLeastV()) {
+            // TODO(b/325469910) : Add cryptoObject tests once opId is retrievable
+            assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+                .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+        } else {
+            assertThat(entry.biometricPromptData).isNull()
+        }
     }
 
     companion object {
@@ -355,5 +401,13 @@
         private val AFFILIATED_DOMAIN = "affiliation-name"
         private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
         private const val SINGLE_PROVIDER_ICON_BIT = true
+
+        @RequiresApi(35)
+        private fun testBiometricPromptData(): BiometricPromptData {
+            return BiometricPromptData.Builder()
+                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build()
+        }
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
index 43cbd13..fff3b7e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
@@ -25,17 +25,21 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 
+import androidx.annotation.RequiresApi;
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.os.BuildCompat;
 import androidx.credentials.PublicKeyCredential;
 import androidx.credentials.R;
 import androidx.credentials.TestUtilsKt;
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption;
+import androidx.credentials.provider.BiometricPromptData;
 import androidx.credentials.provider.PublicKeyCredentialEntry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -46,8 +50,11 @@
 import org.junit.runner.RunWith;
 
 import java.time.Instant;
+
+import javax.crypto.NullCipher;
+
 @RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @SmallTest
 public class PublicKeyCredentialEntryJavaTest {
     private static final CharSequence USERNAME = "title";
@@ -64,8 +71,10 @@
                     "{\"key1\":{\"key2\":{\"key3\":\"value3\"}}}");
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final Intent mIntent = new Intent();
-    private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
+    private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0,
+            mIntent,
             PendingIntent.FLAG_IMMUTABLE);
+
     @Test
     public void build_requiredParamsOnly_success() {
         PublicKeyCredentialEntry entry = constructWithRequiredParamsOnly();
@@ -139,9 +148,10 @@
     }
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromCredentialEntry_success() {
         PublicKeyCredentialEntry originalEntry = constructWithAllParams();
-        Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
         assertNotNull(slice);
         PublicKeyCredentialEntry entry = PublicKeyCredentialEntry.fromCredentialEntry(
                 new android.service.credentials.CredentialEntry("id", slice));
@@ -160,10 +170,11 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
         PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
                 .Builder(mContext, USERNAME, mPendingIntent, mBeginOption).build();
-        Slice slice = PublicKeyCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
 
         assertNotNull(slice);
 
@@ -175,11 +186,12 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 28)
+    @SuppressWarnings("deprecation")
     public void isDefaultIcon_customIconAfterSlice_returnsFalse() {
         PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
                 .Builder(mContext, USERNAME, mPendingIntent, mBeginOption)
                 .setIcon(ICON).build();
-        Slice slice = PublicKeyCredentialEntry.toSlice(entry);
+        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
 
         assertNotNull(slice);
 
@@ -222,10 +234,15 @@
                 mBeginOption).build();
     }
     private PublicKeyCredentialEntry constructWithAllParams() {
-        return new PublicKeyCredentialEntry.Builder(mContext, USERNAME, mPendingIntent,
+        PublicKeyCredentialEntry.Builder testBuilder = new PublicKeyCredentialEntry
+                .Builder(mContext, USERNAME, mPendingIntent,
                 mBeginOption).setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED).setDisplayName(
                 DISPLAYNAME).setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME)).setIcon(
-                ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT).build();
+                ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
+        if (BuildCompat.isAtLeastV()) {
+            testBuilder.setBiometricPromptData(testBiometricPromptData());
+        }
+        return testBuilder.build();
     }
     private void assertEntryWithRequiredParams(PublicKeyCredentialEntry entry) {
         assertThat(USERNAME.equals(entry.getUsername()));
@@ -234,6 +251,7 @@
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT);
         assertThat(entry.getAffiliatedDomain()).isNull();
         assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+        assertThat(entry.getBiometricPromptData()).isNull();
     }
     private void assertEntryWithAllParams(PublicKeyCredentialEntry entry) {
         assertThat(USERNAME.equals(entry.getUsername()));
@@ -247,5 +265,19 @@
                 SINGLE_PROVIDER_ICON_BIT);
         assertThat(entry.getAffiliatedDomain()).isNull();
         assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
+        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
+            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
+                    testBiometricPromptData().getAllowedAuthenticators());
+        } else {
+            assertThat(entry.getBiometricPromptData()).isNull();
+        }
+    }
+
+    @RequiresApi(35)
+    private static BiometricPromptData testBiometricPromptData() {
+        return new BiometricPromptData.Builder()
+                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build();
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
index ed23e72..d4e521e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
@@ -22,11 +22,16 @@
 import android.graphics.drawable.Icon
 import android.os.Bundle
 import android.service.credentials.CredentialEntry
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.core.os.BuildCompat
 import androidx.credentials.CredentialOption
 import androidx.credentials.PublicKeyCredential
 import androidx.credentials.R
 import androidx.credentials.equals
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.PublicKeyCredentialEntry
 import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromCredentialEntry
 import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromSlice
@@ -37,13 +42,14 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
+import javax.crypto.NullCipher
 import junit.framework.TestCase.assertNotNull
 import org.junit.Assert
 import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@SdkSuppress(minSdkVersion = 26)
+@SdkSuppress(minSdkVersion = 26) // Instant usage
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class PublicKeyCredentialEntryTest {
@@ -143,6 +149,7 @@
         assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
         assertThat(entry.affiliatedDomain).isNull()
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     @Test
@@ -247,17 +254,32 @@
     }
 
     private fun constructWithAllParams(): PublicKeyCredentialEntry {
-        return PublicKeyCredentialEntry(
-            mContext,
-            USERNAME,
-            mPendingIntent,
-            BEGIN_OPTION,
-            DISPLAYNAME,
-            Instant.ofEpochMilli(LAST_USED_TIME),
-            ICON,
-            IS_AUTO_SELECT_ALLOWED,
-            SINGLE_PROVIDER_ICON_BIT
-        )
+        return if (BuildCompat.isAtLeastV()) {
+            PublicKeyCredentialEntry(
+                mContext,
+                USERNAME,
+                mPendingIntent,
+                BEGIN_OPTION,
+                DISPLAYNAME,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                SINGLE_PROVIDER_ICON_BIT,
+                testBiometricPromptData()
+            )
+        } else {
+            PublicKeyCredentialEntry(
+                mContext,
+                USERNAME,
+                mPendingIntent,
+                BEGIN_OPTION,
+                DISPLAYNAME,
+                Instant.ofEpochMilli(LAST_USED_TIME),
+                ICON,
+                IS_AUTO_SELECT_ALLOWED,
+                SINGLE_PROVIDER_ICON_BIT,
+            )
+        }
     }
 
     private fun assertEntryWithRequiredParams(entry: PublicKeyCredentialEntry) {
@@ -267,6 +289,7 @@
             .isEqualTo(DEFAULT_SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.affiliatedDomain).isNull()
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        assertThat(entry.biometricPromptData).isNull()
     }
 
     private fun assertEntryWithAllParams(entry: PublicKeyCredentialEntry) {
@@ -280,6 +303,12 @@
         assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
         assertThat(entry.affiliatedDomain).isNull()
         assertThat(entry.entryGroupId).isEqualTo(USERNAME)
+        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
+            assertThat(entry.biometricPromptData!!.allowedAuthenticators)
+                .isEqualTo(testBiometricPromptData().allowedAuthenticators)
+        } else {
+            assertThat(entry.biometricPromptData).isNull()
+        }
     }
 
     companion object {
@@ -298,5 +327,13 @@
         private const val IS_AUTO_SELECT_ALLOWED = true
         private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
         private const val SINGLE_PROVIDER_ICON_BIT = true
+
+        @RequiresApi(35)
+        private fun testBiometricPromptData(): BiometricPromptData {
+            return BiometricPromptData.Builder()
+                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
+                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+                .build()
+        }
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
index 8a44a88..fe81ce4 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertThrows;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.content.Intent;
 
@@ -80,9 +79,10 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 34)
+    @SuppressWarnings("deprecation")
     public void fromRemoteEntry_success() {
         RemoteEntry originalEntry = new RemoteEntry(mPendingIntent);
-        Slice slice = RemoteEntry.toSlice(originalEntry);
+        android.app.slice.Slice slice = RemoteEntry.toSlice(originalEntry);
         assertNotNull(slice);
 
         RemoteEntry remoteEntry = RemoteEntry.fromRemoteEntry(
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
new file mode 100644
index 0000000..2635f98
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:JvmName("CredentialManagerViewHandler")
+
+package androidx.credentials
+
+import android.os.Build
+import android.os.OutcomeReceiver
+import android.util.Log
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.credentials.internal.FrameworkImplHelper
+
+/**
+ * An extension API to the [View] class that allows setting of a [PendingGetCredentialRequest], that
+ * in-turn contains a [GetCredentialRequest], and a callback to deliver the final
+ * [GetCredentialResponse]. The associated request is invoked when the [View] is focused/clicked by
+ * the user.
+ *
+ * A typical scenario for setting this request is a login screen with a username & password field.
+ * We recommend calling the [CredentialManager.getCredential] API when this login screen loads, so
+ * that the user can be presented with a selector with all credential options to choose from. In
+ * addition, we recommend using this API to set the same [GetCredentialRequest] that is passed to
+ * [CredentialManager.getCredential], on the username & password views. With that, if the user
+ * dismisses the initial selector, and then taps on either the username or the password field, they
+ * would see the same suggestions that they saw on the selector, but now on fallback UI experiences
+ * such as keyboard suggestions or drop-down lists, depending on the device capabilities.
+ *
+ * If you have multiple views on the screen that should invoke different requests as opposed to the
+ * same, you can simply use this API to set different requests on corresponding views, and hence a
+ * different set of suggestions will appear.
+ *
+ * Note that no errors are propagated to the [PendingGetCredentialRequest.callback]. In a scenario
+ * where multiple suggestions are presented to the user as part of the keyboard suggestions for
+ * instance, it is possible that the user selects one, but the flow ends up in an error state, due
+ * to which the final [GetCredentialResponse] cannot be propagated. In that case, user will be taken
+ * back to the suggestions, and can very well select a different suggestion which would this time
+ * result in a success. The intermediate error states are not propagated to the developer, and only
+ * a final response, if any, is propagated.
+ *
+ * @property pendingGetCredentialRequest the [GetCredentialRequest] and the associated callback to
+ *   be set on the view, and to be exercised when user focused on the view in question
+ */
+private const val TAG = "ViewHandler"
+
+@Suppress("NewApi")
+var View.pendingGetCredentialRequest: PendingGetCredentialRequest?
+    get() =
+        getTag(R.id.androidx_credential_pendingCredentialRequest) as? PendingGetCredentialRequest
+    set(value) {
+        setTag(R.id.androidx_credential_pendingCredentialRequest, value)
+        if (value != null) {
+            if (
+                Build.VERSION.SDK_INT >= 35 ||
+                    (Build.VERSION.SDK_INT == 34 && Build.VERSION.PREVIEW_SDK_INT > 0)
+            ) {
+                Api35Impl.setPendingGetCredentialRequest(this, value.request, value.callback)
+            }
+        } else {
+            if (
+                Build.VERSION.SDK_INT >= 35 ||
+                    (Build.VERSION.SDK_INT == 34 && Build.VERSION.PREVIEW_SDK_INT > 0)
+            ) {
+                Api35Impl.clearPendingGetCredentialRequest(this)
+            }
+        }
+    }
+
+@RequiresApi(35)
+private object Api35Impl {
+    fun setPendingGetCredentialRequest(
+        view: View,
+        request: GetCredentialRequest,
+        callback: (GetCredentialResponse) -> Unit,
+    ) {
+        val frameworkRequest = FrameworkImplHelper.convertGetRequestToFrameworkClass(request)
+        val frameworkCallback =
+            object :
+                OutcomeReceiver<
+                    android.credentials.GetCredentialResponse,
+                    android.credentials.GetCredentialException
+                > {
+                override fun onResult(response: android.credentials.GetCredentialResponse) {
+                    callback.invoke(FrameworkImplHelper.convertGetResponseToJetpackClass(response))
+                }
+
+                override fun onError(error: android.credentials.GetCredentialException) {
+                    Log.w(TAG, "Error: " + error.type + " , " + error.message)
+                }
+            }
+        view.setPendingCredentialRequest(frameworkRequest, frameworkCallback)
+    }
+
+    fun clearPendingGetCredentialRequest(view: View) {
+        view.clearPendingCredentialRequest()
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index bedcc62..c81075f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -148,7 +148,7 @@
 
         val classNames = mutableListOf<String>()
         if (packageInfo.services != null) {
-            for (serviceInfo in packageInfo.services) {
+            for (serviceInfo in packageInfo.services!!) {
                 if (serviceInfo.metaData != null) {
                     val className = serviceInfo.metaData.getString(CREDENTIAL_PROVIDER_KEY)
                     if (className != null) {
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
new file mode 100644
index 0000000..ae97214
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials
+
+import android.view.View
+
+/**
+ * Request to be set on an Android [View], which will be invoked when the [View] is focused/clicked
+ * by the user.
+ *
+ * Note that the [callback] only handles a final [GetCredentialResponse] and no errors are
+ * propagated to the callback.
+ *
+ * See [View.setPendingCredentialRequest] for details on how this request will be used.
+ *
+ * @property request the [GetCredentialRequest] to be invoked when a given view on which this
+ *   request is set is focused
+ * @property callback the callback on which the final [GetCredentialResponse] is returned, after the
+ *   user has made its selections
+ */
+class PendingGetCredentialRequest(
+    val request: GetCredentialRequest,
+    val callback: (GetCredentialResponse) -> Unit
+)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
index 9800eff..a70417d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
@@ -16,23 +16,28 @@
 
 package androidx.credentials.internal
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.drawable.Icon
+import android.os.Build
 import android.os.Bundle
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
 import androidx.credentials.CreateCredentialRequest
 import androidx.credentials.CreatePasswordRequest
 import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.Credential
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
 import androidx.credentials.R
 
-@RequiresApi(23)
-internal class FrameworkImplHelper {
+@RequiresApi(34)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class FrameworkImplHelper {
     companion object {
         /** Take the create request's `credentialData` and add SDK specific values to it. */
-        @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
         @JvmStatic
-        @RequiresApi(23)
         fun getFinalCreateCredentialData(
             request: CreateCredentialRequest,
             context: Context,
@@ -56,5 +61,52 @@
             )
             return createCredentialData
         }
+
+        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        @JvmStatic
+        fun convertGetResponseToJetpackClass(
+            response: android.credentials.GetCredentialResponse
+        ): GetCredentialResponse {
+            val credential = response.credential
+            return GetCredentialResponse(Credential.createFrom(credential.type, credential.data))
+        }
+
+        @JvmStatic
+        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        fun convertGetRequestToFrameworkClass(
+            request: GetCredentialRequest
+        ): android.credentials.GetCredentialRequest {
+            val builder =
+                android.credentials.GetCredentialRequest.Builder(
+                    GetCredentialRequest.toRequestDataBundle(request)
+                )
+            request.credentialOptions.forEach {
+                builder.addCredentialOption(
+                    android.credentials.CredentialOption.Builder(
+                            it.type,
+                            it.requestData,
+                            it.candidateQueryData
+                        )
+                        .setIsSystemProviderRequired(it.isSystemProviderRequired)
+                        .setAllowedProviders(it.allowedProviders)
+                        .build()
+                )
+            }
+            setOriginForGetRequest(request, builder)
+            return builder.build()
+        }
+
+        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        @SuppressLint("MissingPermission")
+        @VisibleForTesting
+        @JvmStatic
+        fun setOriginForGetRequest(
+            request: GetCredentialRequest,
+            builder: android.credentials.GetCredentialRequest.Builder
+        ) {
+            if (request.origin != null) {
+                builder.setOrigin(request.origin)
+            }
+        }
     }
 }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
index daf3548..2957965 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
+
 package androidx.credentials.provider
 
 import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
index e473b36..fb82267 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
+
 package androidx.credentials.provider
 
 import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
new file mode 100644
index 0000000..6d71c64
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import androidx.annotation.RestrictTo
+import java.util.Objects
+import org.jetbrains.annotations.VisibleForTesting
+
+/**
+ * Error returned from the Biometric Prompt flow that is executed by
+ * [androidx.credentials.CredentialManager] after the user makes a selection on the Credential
+ * Manager account selector.
+ *
+ * @property errorCode the error code denoting what kind of error was encountered while the
+ *   biometric prompt flow failed, must be one of the error codes defined in
+ *   [androidx.biometric.BiometricPrompt] such as
+ *   [androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE] or
+ *   [androidx.biometric.BiometricPrompt.ERROR_TIMEOUT]
+ * @property errorMsg the message associated with the [errorCode] in the form that can be displayed
+ *   on a UI.
+ * @see AuthenticationErrorTypes
+ */
+class AuthenticationError
+@JvmOverloads
+constructor(
+    val errorCode: @AuthenticationErrorTypes Int,
+    val errorMsg: CharSequence? = null,
+) {
+    internal companion object {
+        internal val TAG = "AuthenticationError"
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_ERROR =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK = "BIOMETRIC_AUTH_ERROR_CODE"
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK = "BIOMETRIC_AUTH_ERROR_MESSAGE"
+        // The majority of this is unexpected to be sent, or the values are equal,
+        // but should it arrive for any reason, is handled properly. This way
+        // providers can be confident the Jetpack codes alone are enough.
+        @VisibleForTesting
+        internal val biometricFrameworkToJetpackErrorMap =
+            linkedMapOf(
+                BiometricPrompt.BIOMETRIC_ERROR_CANCELED to
+                    androidx.biometric.BiometricPrompt.ERROR_CANCELED,
+                BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT to
+                    androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT,
+                BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE to
+                    androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE,
+                BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT to
+                    androidx.biometric.BiometricPrompt.ERROR_LOCKOUT,
+                BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to
+                    androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT,
+                BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to
+                    androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS,
+                BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to
+                    androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
+                BiometricPrompt.BIOMETRIC_ERROR_NO_SPACE to
+                    androidx.biometric.BiometricPrompt.ERROR_NO_SPACE,
+                BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to
+                    androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED,
+                BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT to
+                    androidx.biometric.BiometricPrompt.ERROR_TIMEOUT,
+                BiometricPrompt.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to
+                    androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
+                BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED to
+                    androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED,
+                BiometricPrompt.BIOMETRIC_ERROR_VENDOR to
+                    androidx.biometric.BiometricPrompt.ERROR_VENDOR
+                // TODO(b/340334264) : Add NEGATIVE_BUTTON from FW once avail, or wrap this in
+                // a credential manager specific error.
+            )
+
+        internal fun convertFrameworkBiometricErrorToJetpack(frameworkCode: Int): Int {
+            // Ignoring getOrDefault to allow this object down to API 21
+            return if (biometricFrameworkToJetpackErrorMap.containsKey(frameworkCode)) {
+                biometricFrameworkToJetpackErrorMap[frameworkCode]!!
+            } else {
+                Log.i(TAG, "Unexpected error code, $frameworkCode, ")
+                frameworkCode
+            }
+        }
+
+        /**
+         * Generates an instance of this class, to be called by an UI consumer that calls
+         * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
+         * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
+         * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
+         * [isFrameworkBiometricPrompt].
+         *
+         * @param uiErrorCode the error code used to create this error instance, typically using the
+         *   [androidx.biometric.BiometricPrompt]'s constants if conversion isn't desired, or
+         *   [android.hardware.biometrics.BiometricPrompt]'s constants if conversion *is* desired.
+         * @param uiErrorMessage the message associated with the [uiErrorCode] in the form that can
+         *   be displayed on a UI.
+         * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
+         *   requires conversion or not, set to true by default
+         * @return an authentication error that has properly handled conversion of the err code
+         */
+        @JvmOverloads
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        internal fun createFrom(
+            uiErrorCode: Int,
+            uiErrorMessage: CharSequence,
+            isFrameworkBiometricPrompt: Boolean = true,
+        ): AuthenticationError =
+            AuthenticationError(
+                errorCode =
+                    if (isFrameworkBiometricPrompt)
+                        convertFrameworkBiometricErrorToJetpack(uiErrorCode)
+                    else uiErrorCode,
+                errorMsg = uiErrorMessage,
+            )
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other is AuthenticationError) {
+            return this.errorCode == other.errorCode && this.errorMsg == other.errorMsg
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(this.errorCode, this.errorMsg)
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
new file mode 100644
index 0000000..1bcab70
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.ERROR_CANCELED
+import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT
+import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE
+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
+import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT
+import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS
+import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE
+import androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED
+import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT
+import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS
+import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED
+import androidx.biometric.BiometricPrompt.ERROR_VENDOR
+
+/**
+ * This acts as a parameter hint for what [BiometricPrompt]'s error constants should be. You can
+ * learn more about the constants from [BiometricPrompt] to utilize best practices.
+ *
+ * @see BiometricPrompt
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+    value =
+        [
+            ERROR_CANCELED,
+            ERROR_HW_NOT_PRESENT,
+            ERROR_HW_UNAVAILABLE,
+            ERROR_LOCKOUT,
+            ERROR_LOCKOUT_PERMANENT,
+            ERROR_NO_BIOMETRICS,
+            ERROR_NO_DEVICE_CREDENTIAL,
+            ERROR_NO_SPACE,
+            ERROR_SECURITY_UPDATE_REQUIRED,
+            ERROR_TIMEOUT,
+            ERROR_UNABLE_TO_PROCESS,
+            ERROR_USER_CANCELED,
+            ERROR_VENDOR
+        ]
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticationErrorTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
new file mode 100644
index 0000000..8810199
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.credentials.provider.AuthenticationError.Companion.TAG
+import java.util.Objects
+import org.jetbrains.annotations.VisibleForTesting
+
+/**
+ * Successful result returned from the Biometric Prompt authentication flow handled by
+ * [androidx.credentials.CredentialManager].
+ *
+ * @property authenticationType the type of authentication (e.g. device credential or biometric)
+ *   that was requested from and successfully provided by the user, corresponds to constants defined
+ *   in [androidx.biometric.BiometricPrompt] such as
+ *   [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC] or
+ *   [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL]
+ */
+class AuthenticationResult(
+    val authenticationType: @AuthenticationResultTypes Int,
+) {
+    internal companion object {
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE =
+            "androidx.credentials.provider.BIOMETRIC_AUTH_RESULT"
+        @VisibleForTesting
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK = "BIOMETRIC_AUTH_RESULT"
+        @VisibleForTesting
+        internal val biometricFrameworkToJetpackResultMap =
+            linkedMapOf(
+                BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC to
+                    androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
+                BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL to
+                    androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
+                // TODO(b/340334264) : Add TYPE_UNKNOWN once avail from fw, though unexpected unless
+                // very low API level, and may be ignored until jp only impl added in QPR, or other
+                // ctr can be used directly once avail/ready
+            )
+
+        internal fun convertFrameworkBiometricResultToJetpack(frameworkCode: Int): Int {
+            // Ignoring getOrDefault to allow this object down to API 21
+            return if (biometricFrameworkToJetpackResultMap.containsKey(frameworkCode)) {
+                biometricFrameworkToJetpackResultMap[frameworkCode]!!
+            } else {
+                Log.i(TAG, "Non framework result code, $frameworkCode, ")
+                frameworkCode
+            }
+        }
+
+        /**
+         * Generates an instance of this class, to be called by an UI consumer that calls
+         * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
+         * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
+         * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
+         * [isFrameworkBiometricPrompt].
+         *
+         * @param uiAuthenticationType the type of authentication (e.g. device credential or
+         *   biometric) that was requested from and successfully provided by the user, corresponds
+         *   to constants defined in [androidx.biometric.BiometricPrompt] if conversion is not
+         *   desired, or in [android.hardware.biometrics.BiometricPrompt] if conversion is desired
+         * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
+         *   requires conversion or not, set to true by default
+         * @return an authentication result that has properly handled conversion of the result types
+         */
+        @JvmOverloads
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        internal fun createFrom(
+            uiAuthenticationType: Int,
+            isFrameworkBiometricPrompt: Boolean = true,
+        ): AuthenticationResult =
+            AuthenticationResult(
+                authenticationType =
+                    if (isFrameworkBiometricPrompt)
+                        convertFrameworkBiometricResultToJetpack(uiAuthenticationType)
+                    else uiAuthenticationType
+            )
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other is AuthenticationResult) {
+            return this.authenticationType == other.authenticationType
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(this.authenticationType)
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt
new file mode 100644
index 0000000..120688c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResultTypes.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN
+
+/**
+ * This acts as a parameter hint for what [BiometricPrompt]'s result constants should be. You can
+ * learn more about the constants from [BiometricPrompt] to utilize best practices.
+ *
+ * @see BiometricPrompt
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+    value =
+        [
+            AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
+            AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
+            AUTHENTICATION_RESULT_TYPE_UNKNOWN
+        ]
+)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticationResultTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
new file mode 100644
index 0000000..f008628
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import android.hardware.biometrics.BiometricManager
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+
+/**
+ * This allows verification when users pass in [BiometricManager.Authenticators] constants; namely
+ * we can have a parameter hint that indicates what they should be. You can learn more about the
+ * constants from [BiometricManager.Authenticators] to utilize best practices.
+ *
+ * @see BiometricManager.Authenticators
+ */
+@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(value = [BIOMETRIC_STRONG, BIOMETRIC_WEAK, DEVICE_CREDENTIAL])
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+annotation class AuthenticatorTypes
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
new file mode 100644
index 0000000..c279cca
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.core.os.BuildCompat
+
+/**
+ * Biometric prompt data that can be optionally used to provide information needed for the system to
+ * show a biometric prompt directly embedded into the Credential Manager selector.
+ *
+ * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
+ * will be shown along with a biometric / device credential capture mechanism, on a single dialog,
+ * hence avoiding navigation through multiple screens. When user confirmation is retrieved through
+ * the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]
+ * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or
+ * [CredentialEntry].
+ *
+ * Note that the value of [allowedAuthenticators] together with the features of a given device,
+ * determines whether a biometric auth or a device credential mechanism will / can be shown. The
+ * value for this property is found in [Authenticators].
+ *
+ * @property allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
+ *   the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
+ * @property cryptoObject a crypto object to be unlocked after successful authentication; When set,
+ *   the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
+ *   [IllegalArgumentException] is thrown
+ * @throws IllegalArgumentException if [cryptoObject] is not null, and the [allowedAuthenticators]
+ *   is not set to [BIOMETRIC_STRONG]
+ * @see Authenticators
+ */
+@RequiresApi(35)
+class BiometricPromptData
+internal constructor(
+    val cryptoObject: BiometricPrompt.CryptoObject? = null,
+    val allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK,
+    private var isCreatedFromBundle: Boolean = false,
+) {
+
+    /**
+     * Biometric prompt data that can be optionally used to provide information needed for the
+     * system to show a biometric prompt directly embedded into the Credential Manager selector.
+     *
+     * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
+     * will be shown along with a biometric / device credential capture mechanism, on a single
+     * dialog, hence avoiding navigation through multiple screens. When user confirmation is
+     * retrieved through the aforementioned biometric / device capture mechanism, the
+     * [android.app.PendingIntent] associated with the entry is invoked, and the flow continues as
+     * explained in [CreateEntry] or [CredentialEntry].
+     *
+     * Note that the value of [allowedAuthenticators] together with the features of a given device,
+     * determines whether a biometric auth or a device credential mechanism will / can be shown. The
+     * value for this property is found in [Authenticators].
+     *
+     * @param allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
+     *   the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if not set
+     * @param cryptoObject a crypto object to be unlocked after successful authentication; When set,
+     *   the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else an
+     *   [IllegalArgumentException] is thrown
+     * @throws IllegalArgumentException if [cryptoObject] is not null, and the
+     *   [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
+     * @see Authenticators
+     */
+    @JvmOverloads
+    constructor(
+        cryptoObject: BiometricPrompt.CryptoObject? = null,
+        allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK
+    ) : this(cryptoObject, allowedAuthenticators, isCreatedFromBundle = false)
+
+    init {
+        if (!isCreatedFromBundle) {
+            // This is not expected to throw for certain eligible callers who utilize the
+            // isCreatedFromBundle hidden property.
+            require(ALLOWED_AUTHENTICATOR_VALUES.contains(allowedAuthenticators)) {
+                "The allowed authenticator must be specified according to the BiometricPrompt spec."
+            }
+        }
+        if (cryptoObject != null) {
+            require(isStrongAuthenticationType(allowedAuthenticators)) {
+                "If the cryptoObject is non-null, the allowedAuthenticator value must be " +
+                    "Authenticators.BIOMETRIC_STRONG."
+            }
+        }
+    }
+
+    internal companion object {
+
+        private const val TAG = "BiometricPromptData"
+
+        internal const val BUNDLE_HINT_ALLOWED_AUTHENTICATORS =
+            "androidx.credentials.provider.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"
+
+        internal const val BUNDLE_HINT_CRYPTO_OP_ID =
+            "androidx.credentials.provider.BUNDLE_HINT_CRYPTO_OP_ID"
+
+        /**
+         * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.
+         *
+         * @param bundle the [Bundle] object constructed through [toBundle] method, often
+         */
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun fromBundle(bundle: Bundle): BiometricPromptData? {
+            return try {
+                if (!bundle.containsKey(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)) {
+                    throw IllegalArgumentException("Bundle lacks allowed authenticator key.")
+                }
+                if (BuildCompat.isAtLeastV()) {
+                    Api35Impl.fromBundle(bundle)
+                } else {
+                    ApiMinImpl.fromBundle(bundle)
+                }
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+
+        /** Returns a [Bundle] that contains the [BiometricPromptData] representation. */
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+            return if (BuildCompat.isAtLeastV()) {
+                Api35Impl.toBundle(biometricPromptData)
+            } else {
+                ApiMinImpl.toBundle(biometricPromptData)
+            }
+        }
+
+        private fun isStrongAuthenticationType(authenticationTypes: Int?): Boolean {
+            if (authenticationTypes == null) {
+                return false
+            }
+            val biometricStrength: Int = authenticationTypes and BIOMETRIC_WEAK
+            if (biometricStrength and BiometricManager.Authenticators.BIOMETRIC_STRONG.inv() != 0) {
+                return false
+            }
+            return true
+        }
+
+        private val ALLOWED_AUTHENTICATOR_VALUES =
+            setOf(
+                BIOMETRIC_STRONG,
+                BIOMETRIC_WEAK,
+                DEVICE_CREDENTIAL,
+                BIOMETRIC_STRONG or DEVICE_CREDENTIAL,
+                BIOMETRIC_WEAK or DEVICE_CREDENTIAL
+            )
+    }
+
+    /** Builder for constructing an instance of [BiometricPromptData] */
+    class Builder {
+        private var cryptoObject: CryptoObject? = null
+        private var allowedAuthenticators: Int? = null
+
+        /**
+         * Sets whether this [BiometricPromptData] should have a crypto object associated with this
+         * authentication. If opting to pass in a value, the [allowedAuthenticators] must be
+         * [BIOMETRIC_STRONG].
+         *
+         * @param cryptoObject the [CryptoObject] to be associated with this biometric
+         *   authentication flow
+         */
+        fun setCryptoObject(cryptoObject: CryptoObject): Builder {
+            this.cryptoObject = cryptoObject
+            return this
+        }
+
+        /**
+         * Specifies the type(s) of authenticators that may be invoked to authenticate the user.
+         * Available authenticator types are defined in [Authenticators] and can be combined via
+         * bitwise OR. Defaults to [BIOMETRIC_WEAK].
+         *
+         * If this method is used and no authenticator of any of the specified types is available at
+         * the time an error code will be supplied as part of [android.content.Intent] that will be
+         * launched by the containing [CredentialEntry] or [CreateEntry]'s corresponding
+         * [android.app.PendingIntent].
+         *
+         * @param allowedAuthenticators A bit field representing all valid authenticator types that
+         *   may be invoked by the Credential Manager selector.
+         */
+        fun setAllowedAuthenticators(allowedAuthenticators: @AuthenticatorTypes Int): Builder {
+            this.allowedAuthenticators = allowedAuthenticators
+            return this
+        }
+
+        /**
+         * Builds the [BiometricPromptData] instance.
+         *
+         * @throws IllegalArgumentException If [cryptoObject] is not null, and the
+         *   [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
+         */
+        fun build(): BiometricPromptData {
+            val allowedAuthenticators = this.allowedAuthenticators ?: BIOMETRIC_WEAK
+            return BiometricPromptData(
+                cryptoObject = cryptoObject,
+                allowedAuthenticators = allowedAuthenticators,
+            )
+        }
+    }
+
+    private object ApiMinImpl {
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+            val bundle = Bundle()
+            bundle.putInt(
+                BUNDLE_HINT_ALLOWED_AUTHENTICATORS,
+                biometricPromptData.allowedAuthenticators
+            )
+            return bundle
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun fromBundle(bundle: Bundle): BiometricPromptData {
+            val biometricPromptData =
+                BiometricPromptData(
+                    allowedAuthenticators = bundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS),
+                    isCreatedFromBundle = true,
+                )
+            return biometricPromptData
+        }
+    }
+
+    private object Api35Impl {
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun toBundle(biometricPromptData: BiometricPromptData): Bundle {
+            val bundle = Bundle()
+            bundle.putInt(
+                BUNDLE_HINT_ALLOWED_AUTHENTICATORS,
+                biometricPromptData.allowedAuthenticators
+            )
+            biometricPromptData.cryptoObject?.let {
+                bundle.putLong(BUNDLE_HINT_CRYPTO_OP_ID, it.operationHandle)
+            }
+
+            return bundle
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        fun fromBundle(bundle: Bundle): BiometricPromptData {
+            var cryptoObject: CryptoObject? = null
+            if (bundle.containsKey(BUNDLE_HINT_CRYPTO_OP_ID)) {
+                val opId = bundle.getLong(BUNDLE_HINT_CRYPTO_OP_ID)
+                cryptoObject = CryptoObject(opId)
+            }
+            val biometricPromptData =
+                BiometricPromptData(
+                    allowedAuthenticators = bundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS),
+                    cryptoObject = cryptoObject,
+                    isCreatedFromBundle = true,
+                )
+            return biometricPromptData
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
new file mode 100644
index 0000000..3e70c7a
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider
+
+import java.util.Objects
+
+/**
+ * The result of a Biometric Prompt authentication flow, that is propagated to the provider if the
+ * provider requested for [androidx.credentials.CredentialManager] to handle the authentication
+ * flow.
+ *
+ * An instance of this class will always be part of the final provider request, either the
+ * [ProviderGetCredentialRequest] or the [ProviderCreateCredentialRequest] that the provider
+ * receives after the user selects a [CredentialEntry] or a [CreateEntry] respectively.
+ *
+ * @property isSuccessful whether the result is a success result, in which case
+ *   [authenticationResult] should be non-null
+ * @property authenticationResult the result of the authentication flow, non-null if the
+ *   authentication flow was successful
+ * @property authenticationError error information, non-null if the authentication flow has
+ *   failured, meaning that [isSuccessful] will be false in this case
+ */
+class BiometricPromptResult
+internal constructor(
+    val authenticationResult: AuthenticationResult? = null,
+    val authenticationError: AuthenticationError? = null
+) {
+    val isSuccessful: Boolean = authenticationResult != null
+
+    /**
+     * An unsuccessful biometric prompt result, denoting that authentication has failed.
+     *
+     * @param authenticationError the error that caused the biometric prompt authentication flow to
+     *   fail
+     */
+    constructor(
+        authenticationError: AuthenticationError
+    ) : this(authenticationResult = null, authenticationError = authenticationError)
+
+    /**
+     * A successful biometric prompt result, denoting that authentication has succeeded.
+     *
+     * @param authenticationResult the result after a successful biometric prompt authentication
+     *   operation
+     */
+    constructor(
+        authenticationResult: AuthenticationResult
+    ) : this(authenticationResult = authenticationResult, authenticationError = null)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) {
+            return true
+        }
+        if (other is BiometricPromptResult) {
+            return this.isSuccessful == other.isSuccessful &&
+                this.authenticationResult == other.authenticationResult &&
+                this.authenticationError == other.authenticationError
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(this.isSuccessful, this.authenticationResult, this.authenticationError)
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
index 81ef2ff..e5492bb 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
+
 package androidx.credentials.provider
 
 import android.annotation.SuppressLint
@@ -29,6 +31,8 @@
 import androidx.credentials.CredentialManager
 import androidx.credentials.PasswordCredential
 import androidx.credentials.PublicKeyCredential
+import androidx.credentials.provider.CreateEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
 import java.time.Instant
 import java.util.Collections
 
@@ -40,9 +44,27 @@
  * registered. When user selects this entry, the corresponding [PendingIntent] is fired, and the
  * credential creation can be completed.
  *
+ * @property accountName the name of the account where the credential will be saved
+ * @property pendingIntent the [PendingIntent] that will get invoked when the user selects this
+ *   entry, must be created with a unique request code per entry, with flag
+ *   [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and NOT
+ *   with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+ * @property description the localized description shown on UI about where the credential is stored
+ * @property icon the icon to be displayed with this entry on the UI, must be created using
+ *   [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as the
+ *   latter consumes more memory and may cause undefined behavior due to memory implications on
+ *   internal transactions
+ * @property lastUsedTime the last time the account underlying this entry was used by the user,
+ *   distinguishable up to the milli second mark only such that if two entries have the same
+ *   millisecond precision, they will be considered to have been used at the same time
+ * @property isAutoSelectAllowed whether this entry should be auto selected if it is the only entry
+ *   on the selector
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ *   flow that directly handles the biometric verification and presents back the response; set to
+ *   null by default, so if not opted in, the embedded biometric prompt flow will not show
  * @throws IllegalArgumentException If [accountName] is empty
  */
-@RequiresApi(26)
+@RequiresApi(23)
 class CreateEntry
 internal constructor(
     val accountName: CharSequence,
@@ -51,9 +73,9 @@
     val description: CharSequence?,
     val lastUsedTime: Instant?,
     private val credentialCountInformationMap: MutableMap<String, Int?>,
-    val isAutoSelectAllowed: Boolean
+    val isAutoSelectAllowed: Boolean,
+    val biometricPromptData: BiometricPromptData? = null,
 ) {
-
     /**
      * Creates an entry to be displayed on the selector during create flows.
      *
@@ -92,17 +114,76 @@
         @Suppress("AutoBoxing") totalCredentialCount: Int? = null,
         isAutoSelectAllowed: Boolean = false
     ) : this(
-        accountName,
-        pendingIntent,
-        icon,
-        description,
-        lastUsedTime,
-        mutableMapOf(
-            PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
-            PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
-            TYPE_TOTAL_CREDENTIAL to totalCredentialCount
-        ),
-        isAutoSelectAllowed
+        accountName = accountName,
+        pendingIntent = pendingIntent,
+        icon = icon,
+        description = description,
+        lastUsedTime = lastUsedTime,
+        credentialCountInformationMap =
+            mutableMapOf(
+                PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
+                PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
+                TYPE_TOTAL_CREDENTIAL to totalCredentialCount
+            ),
+        isAutoSelectAllowed = isAutoSelectAllowed
+    )
+
+    /**
+     * Creates an entry to be displayed on the selector during create flows.
+     *
+     * @param accountName the name of the account where the credential will be saved
+     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+     *   entry, must be created with a unique request code per entry, with flag
+     *   [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and
+     *   NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+     * @param description the localized description shown on UI about where the credential is stored
+     * @param icon the icon to be displayed with this entry on the UI, must be created using
+     *   [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as
+     *   the latter consumes more memory and may cause undefined behavior due to memory implications
+     *   on internal transactions
+     * @param lastUsedTime the last time the account underlying this entry was used by the user,
+     *   distinguishable up to the milli second mark only such that if two entries have the same
+     *   millisecond precision, they will be considered to have been used at the same time
+     * @param passwordCredentialCount the no. of password credentials contained by the provider
+     * @param publicKeyCredentialCount the no. of public key credentials contained by the provider
+     * @param totalCredentialCount the total no. of credentials contained by the provider
+     * @param isAutoSelectAllowed whether this entry should be auto selected if it is the only entry
+     *   on the selector
+     * @param biometricPromptData the data that is set optionally to utilize a credential manager
+     *   flow that directly handles the biometric verification and presents back the response; set
+     *   to null by default, so if not opted in, the embedded biometric prompt flow will not show
+     * @constructor constructs an instance of [CreateEntry]
+     * @throws IllegalArgumentException If [accountName] is empty, or if [description] is longer
+     *   than 300 characters (important: make sure your descriptions across all locales are within
+     *   this limit)
+     * @throws NullPointerException If [accountName] or [pendingIntent] is null
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    constructor(
+        accountName: CharSequence,
+        pendingIntent: PendingIntent,
+        description: CharSequence? = null,
+        lastUsedTime: Instant? = null,
+        icon: Icon? = null,
+        @Suppress("AutoBoxing") passwordCredentialCount: Int? = null,
+        @Suppress("AutoBoxing") publicKeyCredentialCount: Int? = null,
+        @Suppress("AutoBoxing") totalCredentialCount: Int? = null,
+        isAutoSelectAllowed: Boolean = false,
+        biometricPromptData: BiometricPromptData? = null,
+    ) : this(
+        accountName = accountName,
+        pendingIntent = pendingIntent,
+        icon = icon,
+        description = description,
+        lastUsedTime = lastUsedTime,
+        credentialCountInformationMap =
+            mutableMapOf(
+                PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
+                PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
+                TYPE_TOTAL_CREDENTIAL to totalCredentialCount
+            ),
+        isAutoSelectAllowed = isAutoSelectAllowed,
+        biometricPromptData = biometricPromptData,
     )
 
     init {
@@ -149,7 +230,6 @@
      */
     class Builder
     constructor(private val accountName: CharSequence, private val pendingIntent: PendingIntent) {
-
         private var credentialCountInformationMap: MutableMap<String, Int?> = mutableMapOf()
         private var icon: Icon? = null
         private var description: CharSequence? = null
@@ -158,6 +238,7 @@
         private var publicKeyCredentialCount: Int? = null
         private var totalCredentialCount: Int? = null
         private var autoSelectAllowed: Boolean = false
+        private var biometricPromptData: BiometricPromptData? = null
 
         /** Sets whether the entry should be auto-selected. The value is false by default. */
         @Suppress("MissingGetterMatchingBuilder")
@@ -237,19 +318,32 @@
         }
 
         /**
+         * Sets the biometric prompt data to optionally utilize a credential manager flow that
+         * directly handles the biometric verification for you and gives you the response; set to
+         * null by default, indicating the default behavior is to not utilize this embedded
+         * biometric prompt flow.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+            this.biometricPromptData = biometricPromptData
+            return this
+        }
+
+        /**
          * Builds an instance of [CreateEntry]
          *
          * @throws IllegalArgumentException If [accountName] is empty
          */
         fun build(): CreateEntry {
             return CreateEntry(
-                accountName,
-                pendingIntent,
-                icon,
-                description,
-                lastUsedTime,
-                credentialCountInformationMap,
-                autoSelectAllowed
+                accountName = accountName,
+                pendingIntent = pendingIntent,
+                icon = icon,
+                description = description,
+                lastUsedTime = lastUsedTime,
+                credentialCountInformationMap = credentialCountInformationMap,
+                isAutoSelectAllowed = autoSelectAllowed,
+                biometricPromptData = biometricPromptData
             )
         }
     }
@@ -263,12 +357,91 @@
         }
     }
 
-    @RequiresApi(28)
-    private object Api28Impl {
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private object Api35Impl {
+        private fun addToSlice(createEntry: CreateEntry, sliceBuilder: Slice.Builder) {
+            val biometricPromptData = createEntry.biometricPromptData
+            if (biometricPromptData != null) {
+                if (requiresSlicePropertiesWorkaround()) {
+                    sliceBuilder.addInt(
+                        biometricPromptData.allowedAuthenticators,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+                    )
+                    biometricPromptData.cryptoObject?.let {
+                        sliceBuilder.addLong(
+                            biometricPromptData.cryptoObject.operationHandle,
+                            /*subType=*/ null,
+                            listOf(SLICE_HINT_CRYPTO_OP_ID)
+                        )
+                    }
+                } else {
+                    val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+                    sliceBuilder.addBundle(
+                        biometricBundle,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+                    )
+                }
+            }
+        }
 
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @JvmStatic
         fun toSlice(createEntry: CreateEntry): Slice {
+            val sliceBuilder = Api28Impl.addToSlice(createEntry)
+            addToSlice(createEntry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        /**
+         * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [addToSlice]
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CreateEntry? {
+            val createEntry = Api28Impl.fromSlice(slice) ?: return null
+            var biometricPromptDataBundle: Bundle? = null
+            slice.items.forEach {
+                if (it.hasHint(CredentialEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+                    biometricPromptDataBundle = it.bundle
+                }
+            }
+            return try {
+                CreateEntry(
+                    accountName = createEntry.accountName,
+                    pendingIntent = createEntry.pendingIntent,
+                    icon = createEntry.icon,
+                    description = createEntry.description,
+                    lastUsedTime = createEntry.lastUsedTime,
+                    credentialCountInformationMap = createEntry.credentialCountInformationMap,
+                    isAutoSelectAllowed = createEntry.isAutoSelectAllowed,
+                    biometricPromptData =
+                        if (biometricPromptDataBundle != null)
+                            BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+                        else null
+                )
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+    }
+
+    @RequiresApi(28)
+    private object Api28Impl {
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmStatic
+        fun toSlice(createEntry: CreateEntry): Slice {
+            val sliceBuilder = addToSlice(createEntry)
+            return sliceBuilder.build()
+        }
+
+        // Specific to only this create entry, but shared across API levels > P
+        fun addToSlice(createEntry: CreateEntry): Slice.Builder {
             val accountName = createEntry.accountName
             val icon = createEntry.icon
             val description = createEntry.description
@@ -276,14 +449,12 @@
             val credentialCountInformationMap = createEntry.credentialCountInformationMap
             val pendingIntent = createEntry.pendingIntent
             val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(SLICE_SPEC_TYPE, REVISION_ID))
-
             val autoSelectAllowed =
                 if (createEntry.isAutoSelectAllowed) {
                     AUTO_SELECT_TRUE_STRING
                 } else {
                     AUTO_SELECT_FALSE_STRING
                 }
-
             sliceBuilder.addText(accountName, /* subType= */ null, listOf(SLICE_HINT_ACCOUNT_NAME))
             if (lastUsedTime != null) {
                 sliceBuilder.addLong(
@@ -320,7 +491,7 @@
                     /*subType=*/ null,
                     listOf(SLICE_HINT_AUTO_SELECT_ALLOWED)
                 )
-            return sliceBuilder.build()
+            return sliceBuilder
         }
 
         @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -359,13 +530,13 @@
             }
             return try {
                 CreateEntry(
-                    accountName!!,
-                    pendingIntent!!,
-                    icon,
-                    description,
-                    lastUsedTime,
-                    credentialCountInfo,
-                    autoSelectAllowed
+                    accountName = accountName!!,
+                    pendingIntent = pendingIntent!!,
+                    icon = icon,
+                    description = description,
+                    lastUsedTime = lastUsedTime,
+                    credentialCountInformationMap = credentialCountInfo,
+                    isAutoSelectAllowed = autoSelectAllowed,
                 )
             } catch (e: Exception) {
                 Log.i(TAG, "fromSlice failed with: " + e.message)
@@ -411,36 +582,30 @@
     companion object {
         private const val TAG = "CreateEntry"
         private const val DESCRIPTION_MAX_CHAR_LIMIT = 300
-
         internal const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
-
         private const val SLICE_HINT_ACCOUNT_NAME =
             "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
-
         private const val SLICE_HINT_NOTE =
             "androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
-
         private const val SLICE_HINT_ICON =
             "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
-
         private const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
             "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
-
         private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
             "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
         private const val SLICE_HINT_PENDING_INTENT =
             "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
-
         private const val SLICE_HINT_AUTO_SELECT_ALLOWED =
             "androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_SELECT_ALLOWED"
-
+        private const val SLICE_HINT_BIOMETRIC_PROMPT_DATA =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA"
+        private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
+        private const val SLICE_HINT_CRYPTO_OP_ID =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_CRYPTO_OP_ID"
         private const val AUTO_SELECT_TRUE_STRING = "true"
-
         private const val AUTO_SELECT_FALSE_STRING = "false"
-
         private const val SLICE_SPEC_TYPE = "CreateEntry"
-
         private const val REVISION_ID = 1
 
         /**
@@ -452,7 +617,9 @@
         @JvmStatic
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         fun toSlice(createEntry: CreateEntry): Slice? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.toSlice(createEntry)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.toSlice(createEntry)
             }
             return null
@@ -466,7 +633,9 @@
         @JvmStatic
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         fun fromSlice(slice: Slice): CreateEntry? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.fromSlice(slice)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.fromSlice(slice)
             }
             return null
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
index 55cdd7d..efddf65 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
@@ -13,9 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
 
 package androidx.credentials.provider
 
+import android.annotation.SuppressLint
 import android.app.slice.Slice
 import android.os.Build
 import androidx.annotation.RequiresApi
@@ -50,6 +52,9 @@
  *   default credential type icon when you are the only available provider; see individual
  *   subclasses for these default icons (e.g. for [PublicKeyCredentialEntry], it is based on
  *   [R.drawable.ic_password])
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ *   flow that directly handles the biometric verification and presents back the response; set to
+ *   null by default, so if not opted in, the embedded biometric prompt flow will not show
  */
 abstract class CredentialEntry
 internal constructor(
@@ -58,8 +63,8 @@
     val entryGroupId: CharSequence,
     val isDefaultIconPreferredAsSingleProvider: Boolean,
     val affiliatedDomain: CharSequence? = null,
+    val biometricPromptData: BiometricPromptData? = null,
 ) {
-
     @RequiresApi(34)
     private object Api34Impl {
         @JvmStatic
@@ -71,7 +76,102 @@
         }
     }
 
+    @RequiresApi(35)
+    internal object Api35Impl {
+        @JvmStatic
+        fun toSlice(entry: CredentialEntry): Slice? {
+            when (entry) {
+                is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
+                is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
+                is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+            }
+            return null
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CredentialEntry? {
+            return try {
+                when (slice.spec?.type) {
+                    TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
+                    TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
+                    else -> CustomCredentialEntry.fromSlice(slice)!!
+                }
+            } catch (e: Exception) {
+                // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+                // password / passkey parsing attempt.
+                CustomCredentialEntry.fromSlice(slice)
+            }
+        }
+    }
+
+    @RequiresApi(28)
+    internal object Api28Impl {
+        @JvmStatic
+        fun toSlice(entry: CredentialEntry): Slice? {
+            when (entry) {
+                is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
+                is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
+                is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+            }
+            return null
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CredentialEntry? {
+            return try {
+                when (slice.spec?.type) {
+                    TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
+                    TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
+                    else -> CustomCredentialEntry.fromSlice(slice)!!
+                }
+            } catch (e: Exception) {
+                // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+                // password / passkey parsing attempt.
+                CustomCredentialEntry.fromSlice(slice)
+            }
+        }
+    }
+
     companion object {
+        internal const val TRUE_STRING = "true"
+        internal const val FALSE_STRING = "false"
+        internal const val REVISION_ID = 1
+        internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        internal const val SLICE_HINT_TITLE =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+        internal const val SLICE_HINT_SUBTITLE =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+        internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        internal const val SLICE_HINT_ICON =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+        internal const val SLICE_HINT_AUTO_ALLOWED =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+        internal const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
+        internal const val SLICE_HINT_OPTION_ID =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
+        internal const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
+        internal const val SLICE_HINT_DEFAULT_ICON_RES_ID =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
+        internal const val SLICE_HINT_AFFILIATED_DOMAIN =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
+        internal const val SLICE_HINT_DEDUPLICATION_ID =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
+        internal const val SLICE_HINT_BIOMETRIC_PROMPT_DATA =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_BIOMETRIC_PROMPT_DATA"
+        internal const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
+        internal const val SLICE_HINT_CRYPTO_OP_ID =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"
 
         /**
          * Converts a framework [android.service.credentials.CredentialEntry] class to a Jetpack
@@ -94,31 +194,26 @@
         }
 
         @JvmStatic
-        @RequiresApi(28)
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         internal fun fromSlice(slice: Slice): CredentialEntry? {
-            return try {
-                when (slice.spec?.type) {
-                    TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntry.fromSlice(slice)!!
-                    TYPE_PUBLIC_KEY_CREDENTIAL -> PublicKeyCredentialEntry.fromSlice(slice)!!
-                    else -> CustomCredentialEntry.fromSlice(slice)!!
-                }
-            } catch (e: Exception) {
-                // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
-                // password / passkey parsing attempt.
-                CustomCredentialEntry.fromSlice(slice)
+            return if (Build.VERSION.SDK_INT >= 35) {
+                Api35Impl.fromSlice(slice)
+            } else if (Build.VERSION.SDK_INT >= 28) {
+                Api28Impl.fromSlice(slice)
+            } else {
+                null
             }
         }
 
         @JvmStatic
-        @RequiresApi(28)
         internal fun toSlice(entry: CredentialEntry): Slice? {
-            when (entry) {
-                is PasswordCredentialEntry -> return PasswordCredentialEntry.toSlice(entry)
-                is PublicKeyCredentialEntry -> return PublicKeyCredentialEntry.toSlice(entry)
-                is CustomCredentialEntry -> return CustomCredentialEntry.toSlice(entry)
+            return if (Build.VERSION.SDK_INT >= 35) {
+                Api35Impl.toSlice(entry)
+            } else if (Build.VERSION.SDK_INT >= 28) {
+                Api28Impl.toSlice(entry)
+            } else {
+                null
             }
-            return null
         }
     }
 }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
index 299c051..0b14f6a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
+
 package androidx.credentials.provider
 
 import android.annotation.SuppressLint
@@ -29,6 +31,8 @@
 import androidx.annotation.RestrictTo
 import androidx.credentials.CredentialOption
 import androidx.credentials.R
+import androidx.credentials.provider.CustomCredentialEntry.Api28Impl.addToSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
 import java.time.Instant
 import java.util.Collections
 
@@ -69,10 +73,13 @@
  *   this entry was created allows this entry to be auto-selected
  * @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
  *   a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ *   flow that directly handles the biometric verification and presents back the response; set to
+ *   null by default, so if not opted in, the embedded biometric prompt flow will not show
  * @throws IllegalArgumentException If [type] or [title] are empty
  * @see CredentialEntry
  */
-@RequiresApi(26)
+@RequiresApi(23)
 class CustomCredentialEntry
 internal constructor(
     override val type: String,
@@ -87,20 +94,21 @@
     isDefaultIconPreferredAsSingleProvider: Boolean,
     entryGroupId: CharSequence? = title,
     affiliatedDomain: CharSequence? = null,
+    biometricPromptData: BiometricPromptData? = null,
     autoSelectAllowedFromOption: Boolean =
         CredentialOption.extractAutoSelectValue(beginGetCredentialOption.candidateQueryData),
     private var isCreatedFromSlice: Boolean = false,
     private var isDefaultIconFromSlice: Boolean = false,
 ) :
     CredentialEntry(
-        type,
-        beginGetCredentialOption,
-        entryGroupId ?: title,
+        type = type,
+        beginGetCredentialOption = beginGetCredentialOption,
+        entryGroupId = entryGroupId ?: title,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
+        biometricPromptData = biometricPromptData
     ) {
     val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
     @get:JvmName("hasDefaultIcon")
     val hasDefaultIcon: Boolean
         get() {
@@ -143,7 +151,8 @@
             ReplaceWith(
                 "CustomCredentialEntry(context, title, pendingIntent," +
                     "beginGetCredentialOption, subtitle, typeDisplayName, lastUsedTime, icon, " +
-                    "isAutoSelectAllowed, entryGroupId, isDefaultIconPreferredAsSingleProvider)"
+                    "isAutoSelectAllowed, entryGroupId, isDefaultIconPreferredAsSingleProvider," +
+                    "biometricPromptData)"
             ),
         level = DeprecationLevel.HIDDEN
     )
@@ -158,6 +167,63 @@
         icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
         @Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
     ) : this(
+        type = beginGetCredentialOption.type,
+        title = title,
+        pendingIntent = pendingIntent,
+        isAutoSelectAllowed = isAutoSelectAllowed,
+        subtitle = subtitle,
+        typeDisplayName = typeDisplayName,
+        icon = icon,
+        lastUsedTime = lastUsedTime,
+        beginGetCredentialOption = beginGetCredentialOption,
+        isDefaultIconPreferredAsSingleProvider = false
+    )
+
+    /**
+     * @param context the context of the calling app, required to retrieve fallback resources
+     * @param title the title shown with this entry on the selector UI
+     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+     *   entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system
+     *   to attach the final request
+     * @param beginGetCredentialOption the option from the original [BeginGetCredentialRequest], for
+     *   which this credential entry is being added
+     * @param subtitle the subTitle shown with this entry on the selector UI
+     * @param lastUsedTime the last used time the credential underlying this entry was used by the
+     *   user, distinguishable up to the milli second mark only such that if two entries have the
+     *   same millisecond precision, they will be considered to have been used at the same time
+     * @param typeDisplayName the friendly name to be displayed on the UI for the type of the
+     *   credential
+     * @param icon the icon to be displayed with this entry on the selector UI, if not set a default
+     *   icon representing a custom credential type is set by the library
+     * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+     *   only one on the UI, only takes effect if the app requesting for credentials also opts for
+     *   auto select
+     * @param entryGroupId an ID to uniquely mark this entry for deduplication or to group entries
+     *   during display, set to [title] by default
+     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+     *   default credential type icon (see the default value of [icon]) when you are the only
+     *   available provider; false by default
+     * @param biometricPromptData the data that is set optionally to utilize a credential manager
+     *   flow that directly handles the biometric verification and presents back the response; set
+     *   to null by default, so if not opted in, the embedded biometric prompt flow will not show
+     * @constructor constructs an instance of [CustomCredentialEntry]
+     * @throws IllegalArgumentException If [type] or [title] are empty
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    constructor(
+        context: Context,
+        title: CharSequence,
+        pendingIntent: PendingIntent,
+        beginGetCredentialOption: BeginGetCredentialOption,
+        subtitle: CharSequence? = null,
+        typeDisplayName: CharSequence? = null,
+        lastUsedTime: Instant? = null,
+        icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
+        @Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
+        entryGroupId: CharSequence = title,
+        isDefaultIconPreferredAsSingleProvider: Boolean = false,
+        biometricPromptData: BiometricPromptData? = null,
+    ) : this(
         beginGetCredentialOption.type,
         title,
         pendingIntent,
@@ -167,7 +233,9 @@
         icon,
         lastUsedTime,
         beginGetCredentialOption,
-        isDefaultIconPreferredAsSingleProvider = false
+        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+        entryGroupId = entryGroupId.ifEmpty { title },
+        biometricPromptData = biometricPromptData,
     )
 
     /**
@@ -208,7 +276,7 @@
         icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
         @Suppress("AutoBoxing") isAutoSelectAllowed: Boolean = false,
         entryGroupId: CharSequence = title,
-        isDefaultIconPreferredAsSingleProvider: Boolean = false
+        isDefaultIconPreferredAsSingleProvider: Boolean = false,
     ) : this(
         beginGetCredentialOption.type,
         title,
@@ -220,7 +288,7 @@
         lastUsedTime,
         beginGetCredentialOption,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
-        entryGroupId.ifEmpty { title },
+        entryGroupId = entryGroupId.ifEmpty { title },
     )
 
     @RequiresApi(34)
@@ -234,6 +302,94 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private object Api35Impl {
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmStatic
+        fun toSlice(entry: CustomCredentialEntry): Slice {
+            val type = entry.type
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            Api28Impl.addToSlice(entry, sliceBuilder)
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Given multiple API dependencies, this captures common builds across all API levels > V
+        // and across all subclasses for the toSlice method
+        fun addToSlice(entry: CustomCredentialEntry, sliceBuilder: Slice.Builder) {
+            val biometricPromptData = entry.biometricPromptData
+            if (biometricPromptData != null) {
+                if (requiresSlicePropertiesWorkaround()) {
+                    sliceBuilder.addInt(
+                        biometricPromptData.allowedAuthenticators,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+                    )
+                    biometricPromptData.cryptoObject?.let {
+                        sliceBuilder.addLong(
+                            biometricPromptData.cryptoObject.operationHandle,
+                            /*subType=*/ null,
+                            listOf(SLICE_HINT_CRYPTO_OP_ID)
+                        )
+                    }
+                } else {
+                    val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+                    sliceBuilder.addBundle(
+                        biometricBundle,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+                    )
+                }
+            }
+        }
+
+        /**
+         * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [addToSlice]
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CustomCredentialEntry? {
+            val customCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+            var biometricPromptDataBundle: Bundle? = null
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+                    biometricPromptDataBundle = it.bundle
+                }
+            }
+            return try {
+                CustomCredentialEntry(
+                    type = customCredentialEntry.type,
+                    title = customCredentialEntry.title,
+                    pendingIntent = customCredentialEntry.pendingIntent,
+                    isAutoSelectAllowed = customCredentialEntry.isAutoSelectAllowed,
+                    subtitle = customCredentialEntry.subtitle,
+                    typeDisplayName = customCredentialEntry.typeDisplayName,
+                    icon = customCredentialEntry.icon,
+                    lastUsedTime = customCredentialEntry.lastUsedTime,
+                    beginGetCredentialOption = customCredentialEntry.beginGetCredentialOption,
+                    isDefaultIconPreferredAsSingleProvider =
+                        customCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+                    entryGroupId = customCredentialEntry.entryGroupId,
+                    affiliatedDomain = customCredentialEntry.affiliatedDomain,
+                    autoSelectAllowedFromOption =
+                        customCredentialEntry.isAutoSelectAllowedFromOption,
+                    isCreatedFromSlice = true,
+                    isDefaultIconFromSlice = customCredentialEntry.isDefaultIconFromSlice,
+                    biometricPromptData =
+                        if (biometricPromptDataBundle != null)
+                            BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+                        else null
+                )
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+    }
+
     @RequiresApi(28)
     private object Api28Impl {
         @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -250,6 +406,41 @@
         @JvmStatic
         fun toSlice(entry: CustomCredentialEntry): Slice {
             val type = entry.type
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Specific to only this custom credential entry, but shared across API levels > P
+        fun addToSlice(entry: CustomCredentialEntry, sliceBuilder: Slice.Builder) {
+            val beginGetCredentialOption = entry.beginGetCredentialOption
+            val entryGroupId = entry.entryGroupId
+            val isDefaultIconPreferredAsSingleProvider =
+                entry.isDefaultIconPreferredAsSingleProvider
+            val affiliatedDomain = entry.affiliatedDomain
+            val isUsingDefaultIcon =
+                if (isDefaultIconPreferredAsSingleProvider) {
+                    TRUE_STRING
+                } else {
+                    FALSE_STRING
+                }
+            sliceBuilder
+                .addText(
+                    beginGetCredentialOption.id,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_OPTION_ID)
+                )
+                .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+                .addText(
+                    isUsingDefaultIcon,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+                )
+                .addText(
+                    affiliatedDomain,
+                    /*subTypes=*/ null,
+                    listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+                )
             val title = entry.title
             val subtitle = entry.subtitle
             val pendingIntent = entry.pendingIntent
@@ -257,61 +448,18 @@
             val lastUsedTime = entry.lastUsedTime
             val icon = entry.icon
             val isAutoSelectAllowed = entry.isAutoSelectAllowed
-            val beginGetCredentialOption = entry.beginGetCredentialOption
-            val entryGroupId = entry.entryGroupId
-            val affiliatedDomain = entry.affiliatedDomain
-            val isDefaultIconPreferredAsSingleProvider =
-                entry.isDefaultIconPreferredAsSingleProvider
-
             val autoSelectAllowed =
                 if (isAutoSelectAllowed) {
                     TRUE_STRING
                 } else {
                     FALSE_STRING
                 }
-
-            val isUsingDefaultIconPreferred =
-                if (isDefaultIconPreferredAsSingleProvider) {
-                    TRUE_STRING
-                } else {
-                    FALSE_STRING
-                }
-            val sliceBuilder =
-                Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
-                    .addText(
-                        typeDisplayName,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
-                    )
-                    .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
-                    .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
-                    .addText(
-                        autoSelectAllowed,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_AUTO_ALLOWED)
-                    )
-                    .addText(
-                        beginGetCredentialOption.id,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_OPTION_ID)
-                    )
-                    .addText(
-                        entryGroupId,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_DEDUPLICATION_ID)
-                    )
-                    .addText(
-                        affiliatedDomain,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_AFFILIATED_DOMAIN)
-                    )
-                    .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
-                    .addText(
-                        isUsingDefaultIconPreferred,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
-                    )
-
+            sliceBuilder
+                .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+                .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+                .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+                .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+                .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
             try {
                 if (entry.hasDefaultIcon) {
                     sliceBuilder.addInt(
@@ -321,7 +469,6 @@
                     )
                 }
             } catch (_: IllegalStateException) {}
-
             if (entry.isAutoSelectAllowedFromOption) {
                 sliceBuilder.addInt(
                     /*true=*/ 1,
@@ -343,19 +490,22 @@
                     .build(),
                 /*subType=*/ null
             )
-            return sliceBuilder.build()
         }
 
         /**
          * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
          *
-         * @param slice the [Slice] object constructed through [toSlice]
+         * @param slice the [Slice] object constructed through [addToSlice]
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
         @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
         @JvmStatic
         fun fromSlice(slice: Slice): CustomCredentialEntry? {
             val type: String = slice.spec!!.type
+            var entryGroupId: CharSequence? = null
+            var affiliatedDomain: CharSequence? = null
+            var isDefaultIconPreferredAsSingleProvider = false
+            var beginGetCredentialOptionId: CharSequence? = null
             var typeDisplayName: CharSequence? = null
             var title: CharSequence? = null
             var subtitle: CharSequence? = null
@@ -363,15 +513,21 @@
             var pendingIntent: PendingIntent? = null
             var lastUsedTime: Instant? = null
             var autoSelectAllowed = false
-            var beginGetCredentialOptionId: CharSequence? = null
-            var entryGroupId: CharSequence? = null
             var autoSelectAllowedFromOption = false
-            var isDefaultIconPreferredAsSingleProvider = false
             var isDefaultIcon = false
-            var affiliatedDomain: CharSequence? = null
-
             slice.items.forEach {
-                if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+                if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+                    beginGetCredentialOptionId = it.text
+                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+                    entryGroupId = it.text
+                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+                    val defaultIconValue = it.text
+                    if (defaultIconValue == TRUE_STRING) {
+                        isDefaultIconPreferredAsSingleProvider = true
+                    }
+                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+                    affiliatedDomain = it.text
+                } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
                     typeDisplayName = it.text
                 } else if (it.hasHint(SLICE_HINT_TITLE)) {
                     title = it.text
@@ -381,8 +537,6 @@
                     icon = it.icon
                 } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
                     pendingIntent = it.action
-                } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
-                    beginGetCredentialOptionId = it.text
                 } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
                     lastUsedTime = Instant.ofEpochMilli(it.long)
                 } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -390,22 +544,12 @@
                     if (autoSelectValue == TRUE_STRING) {
                         autoSelectAllowed = true
                     }
-                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
-                    entryGroupId = it.text
                 } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
                     autoSelectAllowedFromOption = true
-                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
-                    val defaultIconValue = it.text
-                    if (defaultIconValue == TRUE_STRING) {
-                        isDefaultIconPreferredAsSingleProvider = true
-                    }
                 } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
                     isDefaultIcon = true
-                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
-                    affiliatedDomain = it.text
                 }
             }
-
             return try {
                 CustomCredentialEntry(
                     type = type,
@@ -439,51 +583,6 @@
     companion object {
         private const val TAG = "CredentialEntry"
 
-        private const val SLICE_HINT_TYPE_DISPLAY_NAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_TITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
-        private const val SLICE_HINT_SUBTITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
-        private const val SLICE_HINT_ICON =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
-        private const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
-        private const val SLICE_HINT_AUTO_ALLOWED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
-        private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
-        private const val SLICE_HINT_OPTION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
-        private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
-        private const val SLICE_HINT_DEDUPLICATION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
-        private const val SLICE_HINT_AFFILIATED_DOMAIN =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
-        private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
-        private const val TRUE_STRING = "true"
-
-        private const val FALSE_STRING = "false"
-
-        private const val REVISION_ID = 1
-
         /**
          * Converts an instance of [CustomCredentialEntry] to a [Slice].
          *
@@ -493,7 +592,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @JvmStatic
         fun toSlice(entry: CustomCredentialEntry): Slice? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.toSlice(entry)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.toSlice(entry)
             }
             return null
@@ -502,13 +603,15 @@
         /**
          * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
          *
-         * @param slice the [Slice] object constructed through [toSlice]
+         * @param slice the [Slice] object constructed through [addToSlice]
          */
         @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
         @JvmStatic
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         fun fromSlice(slice: Slice): CustomCredentialEntry? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.fromSlice(slice)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.fromSlice(slice)
             }
             return null
@@ -560,6 +663,7 @@
         private var autoSelectAllowed = false
         private var entryGroupId: CharSequence = title
         private var isDefaultIconPreferredAsSingleProvider = false
+        private var biometricPromptData: BiometricPromptData? = null
 
         /** Sets a displayName to be shown on the UI with this entry. */
         fun setSubtitle(subtitle: CharSequence?): Builder {
@@ -582,6 +686,18 @@
             return this
         }
 
+        /**
+         * Sets the biometric prompt data to optionally utilize a credential manager flow that
+         * directly handles the biometric verification for you and gives you the response; set to
+         * null by default, indicating the default behavior is to not utilize this embedded
+         * biometric prompt flow.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+            this.biometricPromptData = biometricPromptData
+            return this
+        }
+
         /** Sets whether the entry should be auto-selected. The value is false by default. */
         @Suppress("MissingGetterMatchingBuilder")
         fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -627,17 +743,18 @@
                 icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in)
             }
             return CustomCredentialEntry(
-                type,
-                title,
-                pendingIntent,
-                autoSelectAllowed,
-                subtitle,
-                typeDisplayName,
-                icon!!,
-                lastUsedTime,
-                beginGetCredentialOption,
+                type = type,
+                title = title,
+                pendingIntent = pendingIntent,
+                isAutoSelectAllowed = autoSelectAllowed,
+                subtitle = subtitle,
+                typeDisplayName = typeDisplayName,
+                icon = icon!!,
+                lastUsedTime = lastUsedTime,
+                beginGetCredentialOption = beginGetCredentialOption,
                 isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
                 entryGroupId = entryGroupId,
+                biometricPromptData = biometricPromptData,
             )
         }
     }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
index 034b0af..0d17857 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
 
 package androidx.credentials.provider
 
@@ -31,7 +32,9 @@
 import androidx.credentials.CredentialOption
 import androidx.credentials.PasswordCredential
 import androidx.credentials.R
+import androidx.credentials.provider.PasswordCredentialEntry.Api28Impl.toSlice
 import androidx.credentials.provider.PasswordCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
 import java.time.Instant
 import java.util.Collections
 
@@ -64,11 +67,14 @@
  *   this entry was created allows this entry to be auto-selected
  * @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
  *   a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ *   flow that directly handles the biometric verification and presents back the response; set to
+ *   null by default, so if not opted in, the embedded biometric prompt flow will not show
  * @throws IllegalArgumentException If [username] is empty
  * @see CustomCredentialEntry
  * @see CredentialEntry
  */
-@RequiresApi(26)
+@RequiresApi(23)
 class PasswordCredentialEntry
 internal constructor(
     val username: CharSequence,
@@ -82,21 +88,21 @@
     isDefaultIconPreferredAsSingleProvider: Boolean,
     entryGroupId: CharSequence? = username,
     affiliatedDomain: CharSequence? = null,
+    biometricPromptData: BiometricPromptData? = null,
     autoSelectAllowedFromOption: Boolean =
         CredentialOption.extractAutoSelectValue(beginGetPasswordOption.candidateQueryData),
     private var isCreatedFromSlice: Boolean = false,
     private var isDefaultIconFromSlice: Boolean = false
 ) :
     CredentialEntry(
-        PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
-        beginGetPasswordOption,
-        entryGroupId ?: username,
+        type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+        beginGetCredentialOption = beginGetPasswordOption,
+        entryGroupId = entryGroupId ?: username,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
+        biometricPromptData = biometricPromptData,
     ) {
-
     val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
     @get:JvmName("hasDefaultIcon")
     val hasDefaultIcon: Boolean
         get() {
@@ -185,6 +191,70 @@
      * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
      *   only one on the UI, only takes effect if the app requesting for credentials also opts for
      *   auto select
+     * @param affiliatedDomain the user visible affiliated domain, a CharSequence representation of
+     *   a web domain or an app package name that the given credential in this entry is associated
+     *   with when it is different from the requesting entity, default null
+     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+     *   default credential type icon (see the default value of [icon]) when you are the only
+     *   available provider; false by default
+     * @param biometricPromptData the data that is set optionally to utilize a credential manager
+     *   flow that directly handles the biometric verification and presents back the response; set
+     *   to null by default, so if not opted in, the embedded biometric prompt flow will not show
+     * @constructor constructs an instance of [PasswordCredentialEntry]
+     *
+     * The [affiliatedDomain] parameter is filled if you provide a credential that is not directly
+     * associated with the requesting entity, but rather originates from an entity that is
+     * determined as being associated with the requesting entity through mechanisms such as digital
+     * asset links.
+     *
+     * @throws IllegalArgumentException If [username] is empty
+     * @throws NullPointerException If [context], [username], [pendingIntent], or
+     *   [beginGetPasswordOption] is null
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    constructor(
+        context: Context,
+        username: CharSequence,
+        pendingIntent: PendingIntent,
+        beginGetPasswordOption: BeginGetPasswordOption,
+        displayName: CharSequence? = null,
+        lastUsedTime: Instant? = null,
+        icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
+        isAutoSelectAllowed: Boolean = false,
+        affiliatedDomain: CharSequence? = null,
+        isDefaultIconPreferredAsSingleProvider: Boolean = false,
+        biometricPromptData: BiometricPromptData? = null
+    ) : this(
+        username,
+        displayName,
+        typeDisplayName = context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL),
+        pendingIntent,
+        lastUsedTime,
+        icon,
+        isAutoSelectAllowed,
+        beginGetPasswordOption,
+        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+        affiliatedDomain = affiliatedDomain,
+        biometricPromptData = biometricPromptData,
+    )
+
+    /**
+     * @param context the context of the calling app, required to retrieve fallback resources
+     * @param username the username of the account holding the password credential
+     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+     *   entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system
+     *   to attach the final request
+     * @param beginGetPasswordOption the option from the original [BeginGetCredentialRequest], for
+     *   which this credential entry is being added
+     * @param displayName the displayName of the account holding the password credential
+     * @param lastUsedTime the last used time the credential underlying this entry was used by the
+     *   user, distinguishable up to the milli second mark only such that if two entries have the
+     *   same millisecond precision, they will be considered to have been used at the same time
+     * @param icon the icon to be displayed with this entry on the selector, if not set, a default
+     *   icon representing a password credential type is set by the library
+     * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+     *   only one on the UI, only takes effect if the app requesting for credentials also opts for
+     *   auto select
      * @constructor constructs an instance of [PasswordCredentialEntry]
      * @throws IllegalArgumentException If [username] is empty
      * @throws NullPointerException If [context], [username], [pendingIntent], or
@@ -196,7 +266,8 @@
             ReplaceWith(
                 "PasswordCredentialEntry(context, username, " +
                     "pendingIntent, beginGetPasswordOption, displayName, lastUsedTime, icon, " +
-                    "isAutoSelectAllowed, affiliatedDomain, isDefaultIconPreferredAsSingleProvider)"
+                    "isAutoSelectAllowed, affiliatedDomain, isDefaultIconPreferredAsSingleProvider, " +
+                    "biometricPromptData)"
             ),
         level = DeprecationLevel.HIDDEN
     )
@@ -210,14 +281,14 @@
         icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
         isAutoSelectAllowed: Boolean = false
     ) : this(
-        username,
-        displayName,
+        username = username,
+        displayName = displayName,
         typeDisplayName = context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL),
-        pendingIntent,
-        lastUsedTime,
-        icon,
-        isAutoSelectAllowed,
-        beginGetPasswordOption,
+        pendingIntent = pendingIntent,
+        lastUsedTime = lastUsedTime,
+        icon = icon,
+        isAutoSelectAllowed = isAutoSelectAllowed,
+        beginGetPasswordOption = beginGetPasswordOption,
         isDefaultIconPreferredAsSingleProvider = false
     )
 
@@ -232,6 +303,94 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private object Api35Impl {
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmStatic
+        fun toSlice(entry: PasswordCredentialEntry): Slice {
+            val type = entry.type
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            Api28Impl.addToSlice(entry, sliceBuilder)
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Given multiple API dependencies, this captures common builds across all API levels > V
+        // and across all subclasses for the toSlice method
+        fun addToSlice(entry: PasswordCredentialEntry, sliceBuilder: Slice.Builder) {
+            val biometricPromptData = entry.biometricPromptData
+            if (biometricPromptData != null) {
+                if (requiresSlicePropertiesWorkaround()) {
+                    sliceBuilder.addInt(
+                        biometricPromptData.allowedAuthenticators,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+                    )
+                    biometricPromptData.cryptoObject?.let {
+                        sliceBuilder.addLong(
+                            biometricPromptData.cryptoObject.operationHandle,
+                            /*subType=*/ null,
+                            listOf(SLICE_HINT_CRYPTO_OP_ID)
+                        )
+                    }
+                } else {
+                    val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+                    sliceBuilder.addBundle(
+                        biometricBundle,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+                    )
+                }
+            }
+        }
+
+        /**
+         * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): PasswordCredentialEntry? {
+            val passwordCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+            var biometricPromptDataBundle: Bundle? = null
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+                    biometricPromptDataBundle = it.bundle
+                }
+            }
+            return try {
+                PasswordCredentialEntry(
+                    username = passwordCredentialEntry.username,
+                    displayName = passwordCredentialEntry.displayName,
+                    typeDisplayName = passwordCredentialEntry.typeDisplayName,
+                    pendingIntent = passwordCredentialEntry.pendingIntent,
+                    lastUsedTime = passwordCredentialEntry.lastUsedTime,
+                    icon = passwordCredentialEntry.icon,
+                    isAutoSelectAllowed = passwordCredentialEntry.isAutoSelectAllowed,
+                    beginGetPasswordOption =
+                        passwordCredentialEntry.beginGetCredentialOption as BeginGetPasswordOption,
+                    entryGroupId = passwordCredentialEntry.entryGroupId,
+                    isDefaultIconPreferredAsSingleProvider =
+                        passwordCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+                    affiliatedDomain = passwordCredentialEntry.affiliatedDomain,
+                    autoSelectAllowedFromOption =
+                        passwordCredentialEntry.isAutoSelectAllowedFromOption,
+                    isCreatedFromSlice = true,
+                    isDefaultIconFromSlice = passwordCredentialEntry.isDefaultIconFromSlice,
+                    biometricPromptData =
+                        if (biometricPromptDataBundle != null)
+                            BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+                        else null
+                )
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+    }
+
     @RequiresApi(28)
     private object Api28Impl {
         @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -248,6 +407,41 @@
         @JvmStatic
         fun toSlice(entry: PasswordCredentialEntry): Slice {
             val type = entry.type
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Specific to only this custom credential entry, but shared across API levels > P
+        fun addToSlice(entry: PasswordCredentialEntry, sliceBuilder: Slice.Builder) {
+            val beginGetCredentialOption = entry.beginGetCredentialOption
+            val entryGroupId = entry.entryGroupId
+            val isDefaultIconPreferredAsSingleProvider =
+                entry.isDefaultIconPreferredAsSingleProvider
+            val affiliatedDomain = entry.affiliatedDomain
+            val isUsingDefaultIcon =
+                if (isDefaultIconPreferredAsSingleProvider) {
+                    TRUE_STRING
+                } else {
+                    FALSE_STRING
+                }
+            sliceBuilder
+                .addText(
+                    beginGetCredentialOption.id,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_OPTION_ID)
+                )
+                .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+                .addText(
+                    isUsingDefaultIcon,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+                )
+                .addText(
+                    affiliatedDomain,
+                    /*subTypes=*/ null,
+                    listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+                )
             val title = entry.username
             val subtitle = entry.displayName
             val pendingIntent = entry.pendingIntent
@@ -255,55 +449,18 @@
             val lastUsedTime = entry.lastUsedTime
             val icon = entry.icon
             val isAutoSelectAllowed = entry.isAutoSelectAllowed
-            val beginGetPasswordCredentialOption = entry.beginGetCredentialOption
-            val affiliatedDomain = entry.affiliatedDomain
-            val entryGroupId = entry.entryGroupId
-            var isDefaultIconPreferredAsSingleProvider =
-                entry.isDefaultIconPreferredAsSingleProvider
-
             val autoSelectAllowed =
                 if (isAutoSelectAllowed) {
                     TRUE_STRING
                 } else {
                     FALSE_STRING
                 }
-            val isUsingDefaultIcon =
-                if (isDefaultIconPreferredAsSingleProvider) TRUE_STRING else FALSE_STRING
-            val sliceBuilder =
-                Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
-                    .addText(
-                        typeDisplayName,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
-                    )
-                    .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
-                    .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
-                    .addText(
-                        autoSelectAllowed,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_AUTO_ALLOWED)
-                    )
-                    .addText(
-                        beginGetPasswordCredentialOption.id,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_OPTION_ID)
-                    )
-                    .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
-                    .addText(
-                        entryGroupId,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_DEDUPLICATION_ID)
-                    )
-                    .addText(
-                        affiliatedDomain,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_AFFILIATED_DOMAIN)
-                    )
-                    .addText(
-                        isUsingDefaultIcon,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
-                    )
+            sliceBuilder
+                .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+                .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+                .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+                .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+                .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
             try {
                 if (entry.hasDefaultIcon) {
                     sliceBuilder.addInt(
@@ -313,7 +470,6 @@
                     )
                 }
             } catch (_: IllegalStateException) {}
-
             if (entry.isAutoSelectAllowedFromOption) {
                 sliceBuilder.addInt(
                     /*true=*/ 1,
@@ -335,7 +491,6 @@
                     .build(),
                 /*subType=*/ null
             )
-            return sliceBuilder.build()
         }
 
         /**
@@ -347,33 +502,41 @@
         @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
         @JvmStatic
         fun fromSlice(slice: Slice): PasswordCredentialEntry? {
+            var entryGroupId: CharSequence? = null
+            var affiliatedDomain: CharSequence? = null
+            var isDefaultIconPreferredAsSingleProvider = false
+            var beginGetCredentialOptionId: CharSequence? = null
             var typeDisplayName: CharSequence? = null
             var title: CharSequence? = null
-            var subTitle: CharSequence? = null
+            var subtitle: CharSequence? = null
             var icon: Icon? = null
             var pendingIntent: PendingIntent? = null
             var lastUsedTime: Instant? = null
             var autoSelectAllowed = false
             var autoSelectAllowedFromOption = false
-            var beginGetPasswordOptionId: CharSequence? = null
-            var isDefaultIconPreferredAsSingleProvider = false
-            var affiliatedDomain: CharSequence? = null
-            var entryGroupId: CharSequence? = null
             var isDefaultIcon = false
-
             slice.items.forEach {
-                if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+                if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+                    beginGetCredentialOptionId = it.text
+                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+                    entryGroupId = it.text
+                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+                    val defaultIconValue = it.text
+                    if (defaultIconValue == TRUE_STRING) {
+                        isDefaultIconPreferredAsSingleProvider = true
+                    }
+                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+                    affiliatedDomain = it.text
+                } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
                     typeDisplayName = it.text
                 } else if (it.hasHint(SLICE_HINT_TITLE)) {
                     title = it.text
                 } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
-                    subTitle = it.text
+                    subtitle = it.text
                 } else if (it.hasHint(SLICE_HINT_ICON)) {
                     icon = it.icon
                 } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
                     pendingIntent = it.action
-                } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
-                    beginGetPasswordOptionId = it.text
                 } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
                     lastUsedTime = Instant.ofEpochMilli(it.long)
                 } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -383,24 +546,14 @@
                     }
                 } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
                     autoSelectAllowedFromOption = true
-                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
-                    affiliatedDomain = it.text
-                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
-                    entryGroupId = it.text
-                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
-                    val defaultIconValue = it.text
-                    if (defaultIconValue == TRUE_STRING) {
-                        isDefaultIconPreferredAsSingleProvider = true
-                    }
                 } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
                     isDefaultIcon = true
                 }
             }
-
             return try {
                 PasswordCredentialEntry(
                     username = title!!,
-                    displayName = subTitle,
+                    displayName = subtitle,
                     typeDisplayName = typeDisplayName!!,
                     pendingIntent = pendingIntent!!,
                     lastUsedTime = lastUsedTime,
@@ -409,7 +562,7 @@
                     beginGetPasswordOption =
                         BeginGetPasswordOption.createFrom(
                             Bundle(),
-                            beginGetPasswordOptionId!!.toString()
+                            beginGetCredentialOptionId!!.toString()
                         ),
                     entryGroupId = entryGroupId,
                     isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
@@ -428,51 +581,6 @@
     companion object {
         private const val TAG = "PasswordCredentialEntry"
 
-        private const val SLICE_HINT_TYPE_DISPLAY_NAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_TITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
-        private const val SLICE_HINT_SUBTITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
-        private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
-        private const val SLICE_HINT_ICON =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
-        private const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
-        private const val SLICE_HINT_OPTION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
-        private const val SLICE_HINT_AUTO_ALLOWED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
-        private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
-        private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
-        private const val SLICE_HINT_DEDUPLICATION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
-        private const val SLICE_HINT_AFFILIATED_DOMAIN =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
-        private const val TRUE_STRING = "true"
-
-        private const val FALSE_STRING = "false"
-
-        private const val REVISION_ID = 1
-
         /**
          * Converts an instance of [PasswordCredentialEntry] to a [Slice].
          *
@@ -482,7 +590,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @JvmStatic
         fun toSlice(entry: PasswordCredentialEntry): Slice? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.toSlice(entry)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.toSlice(entry)
             }
             return null
@@ -491,7 +601,9 @@
         @JvmStatic
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         fun fromSlice(slice: Slice): PasswordCredentialEntry? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.fromSlice(slice)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.fromSlice(slice)
             }
             return null
@@ -546,6 +658,7 @@
         private var autoSelectAllowed = false
         private var affiliatedDomain: CharSequence? = null
         private var isDefaultIconPreferredAsSingleProvider: Boolean = false
+        private var biometricPromptData: BiometricPromptData? = null
 
         /** Sets a displayName to be shown on the UI with this entry. */
         fun setDisplayName(displayName: CharSequence?): Builder {
@@ -559,6 +672,18 @@
             return this
         }
 
+        /**
+         * Sets the biometric prompt data to optionally utilize a credential manager flow that
+         * directly handles the biometric verification for you and gives you the response; set to
+         * null by default, indicating the default behavior is to not utilize this embedded
+         * biometric prompt flow.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+            this.biometricPromptData = biometricPromptData
+            return this
+        }
+
         /** Sets whether the entry should be auto-selected. The value is false by default. */
         @Suppress("MissingGetterMatchingBuilder")
         fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -605,16 +730,17 @@
             val typeDisplayName =
                 context.getString(R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL)
             return PasswordCredentialEntry(
-                username,
-                displayName,
-                typeDisplayName,
-                pendingIntent,
-                lastUsedTime,
-                icon!!,
-                autoSelectAllowed,
-                beginGetPasswordOption,
+                username = username,
+                displayName = displayName,
+                typeDisplayName = typeDisplayName,
+                pendingIntent = pendingIntent,
+                lastUsedTime = lastUsedTime,
+                icon = icon!!,
+                isAutoSelectAllowed = autoSelectAllowed,
+                beginGetPasswordOption = beginGetPasswordOption,
                 isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
                 affiliatedDomain = affiliatedDomain,
+                biometricPromptData = biometricPromptData,
             )
         }
     }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
index e4971fa..8af4316 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.credentials.provider
 
 import android.app.Activity
@@ -31,6 +30,7 @@
 import androidx.credentials.exceptions.CreateCredentialException
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.provider.utils.BeginGetCredentialUtil
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
 import java.util.stream.Collectors
 
 /**
@@ -77,26 +77,69 @@
                 Log.i(TAG, "Request not found in pendingIntent")
                 return frameworkReq
             }
+            val biometricPromptResult =
+                if (requiresSlicePropertiesWorkaround())
+                    retrieveBiometricPromptResultFallback(intent)
+                else retrieveBiometricPromptResult(intent)
             return try {
                 ProviderCreateCredentialRequest(
-                    androidx.credentials.CreateCredentialRequest.createFrom(
-                        frameworkReq.type,
-                        frameworkReq.data,
-                        frameworkReq.data,
-                        requireSystemProvider = false,
-                        frameworkReq.callingAppInfo.origin
-                    ),
-                    CallingAppInfo(
-                        frameworkReq.callingAppInfo.packageName,
-                        frameworkReq.callingAppInfo.signingInfo,
-                        frameworkReq.callingAppInfo.origin
-                    )
+                    callingRequest =
+                        androidx.credentials.CreateCredentialRequest.createFrom(
+                            frameworkReq.type,
+                            frameworkReq.data,
+                            frameworkReq.data,
+                            requireSystemProvider = false,
+                            frameworkReq.callingAppInfo.origin
+                        ),
+                    callingAppInfo =
+                        CallingAppInfo(
+                            frameworkReq.callingAppInfo.packageName,
+                            frameworkReq.callingAppInfo.signingInfo,
+                            frameworkReq.callingAppInfo.origin
+                        ),
+                    biometricPromptResult = biometricPromptResult
                 )
             } catch (e: IllegalArgumentException) {
                 return null
             }
         }
 
+        private fun retrieveBiometricPromptResult(
+            intent: Intent,
+            resultKey: String? = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE,
+            errorKey: String? = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR,
+            errorMessageKey: String? = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE
+        ): BiometricPromptResult? {
+            if (intent.extras == null) {
+                return null
+            }
+            if (intent.extras!!.containsKey(resultKey)) {
+                val authResultType = intent.extras!!.getInt(resultKey)
+                return BiometricPromptResult(
+                    authenticationResult = AuthenticationResult(authResultType)
+                )
+            } else if (intent.extras!!.containsKey(errorKey)) {
+                val authResultError = intent.extras!!.getInt(errorKey)
+                return BiometricPromptResult(
+                    authenticationError =
+                        AuthenticationError(
+                            authResultError,
+                            intent.extras?.getCharSequence(errorMessageKey)
+                        )
+                )
+            }
+            return null
+        }
+
+        private fun retrieveBiometricPromptResultFallback(intent: Intent): BiometricPromptResult? {
+            return retrieveBiometricPromptResult(
+                intent,
+                resultKey = AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE_FALLBACK,
+                errorKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_FALLBACK,
+                errorMessageKey = AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE_FALLBACK
+            )
+        }
+
         /**
          * Extracts the [BeginGetCredentialRequest] from the provider's [PendingIntent] invoked by
          * the Android system when the user selects an [AuthenticationAction].
@@ -160,7 +203,10 @@
                 Log.i(TAG, "Get request from framework is null")
                 return null
             }
-
+            val biometricPromptResult =
+                if (requiresSlicePropertiesWorkaround())
+                    retrieveBiometricPromptResultFallback(intent)
+                else retrieveBiometricPromptResult(intent)
             return ProviderGetCredentialRequest.createFrom(
                 frameworkReq.credentialOptions
                     .stream()
@@ -178,7 +224,8 @@
                     frameworkReq.callingAppInfo.packageName,
                     frameworkReq.callingAppInfo.signingInfo,
                     frameworkReq.callingAppInfo.origin
-                )
+                ),
+                biometricPromptResult
             )
         }
 
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
index 9077238..aae640f 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.credentials.provider
 
 import androidx.credentials.CreateCredentialRequest
@@ -25,14 +24,22 @@
  * This request contains the actual request coming from the calling app, and the application
  * information associated with the calling app.
  *
- * @param callingRequest the complete [CreateCredentialRequest] coming from the calling app that is
- *   requesting for credential creation
- * @param callingAppInfo information pertaining to the calling app making the request
  * @constructor constructs an instance of [ProviderCreateCredentialRequest]
+ * @property callingRequest the complete [CreateCredentialRequest] coming from the calling app that
+ *   is requesting for credential creation
+ * @property callingAppInfo information pertaining to the calling app making the request
+ * @property biometricPromptResult the result of a Biometric Prompt authentication flow, that is
+ *   propagated to the provider if the provider requested for
+ *   [androidx.credentials.CredentialManager] to handle the authentication flow
  * @throws NullPointerException If [callingRequest], or [callingAppInfo] is null
  *
  * Note : Credential providers are not expected to utilize the constructor in this class for any
  * production flow. This constructor must only be used for testing purposes.
  */
 class ProviderCreateCredentialRequest
-constructor(val callingRequest: CreateCredentialRequest, val callingAppInfo: CallingAppInfo)
+@JvmOverloads
+constructor(
+    val callingRequest: CreateCredentialRequest,
+    val callingAppInfo: CallingAppInfo,
+    val biometricPromptResult: BiometricPromptResult? = null
+)
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
index f733715..ab957ed 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
@@ -27,28 +27,36 @@
  * set on the [CredentialEntry] that the user selected. The request must be extracted using the
  * [PendingIntentHandler.retrieveProviderGetCredentialRequest] helper API.
  *
- * @param credentialOptions the list of credential retrieval options containing the required
+ * @constructor constructs an instance of [ProviderGetCredentialRequest]
+ * @property credentialOptions the list of credential retrieval options containing the required
  *   parameters, expected to contain a single [CredentialOption] when this request is retrieved from
  *   the [android.app.Activity] invoked by the [android.app.PendingIntent] set on a
  *   [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain multiple
  *   [CredentialOption] when this request is retrieved from the [android.app.Activity] invoked by
  *   the [android.app.PendingIntent] set on a [RemoteEntry]
- * @param callingAppInfo information pertaining to the calling application
+ * @property callingAppInfo information pertaining to the calling application
+ * @property biometricPromptResult the result of a Biometric Prompt authentication flow, that is
+ *   propagated to the provider if the provider requested for
+ *   [androidx.credentials.CredentialManager] to handle the authentication flow
  *
  * Note : Credential providers are not expected to utilize the constructor in this class for any
  * production flow. This constructor must only be used for testing purposes.
- *
- * @constructor constructs an instance of [ProviderGetCredentialRequest]
  */
 class ProviderGetCredentialRequest
-constructor(val credentialOptions: List<CredentialOption>, val callingAppInfo: CallingAppInfo) {
+@JvmOverloads
+constructor(
+    val credentialOptions: List<CredentialOption>,
+    val callingAppInfo: CallingAppInfo,
+    val biometricPromptResult: BiometricPromptResult? = null,
+) {
     internal companion object {
         @JvmStatic
         internal fun createFrom(
             options: List<CredentialOption>,
-            callingAppInfo: CallingAppInfo
+            callingAppInfo: CallingAppInfo,
+            biometricPromptResult: BiometricPromptResult? = null
         ): ProviderGetCredentialRequest {
-            return ProviderGetCredentialRequest(options, callingAppInfo)
+            return ProviderGetCredentialRequest(options, callingAppInfo, biometricPromptResult)
         }
     }
 }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
index 8d1b8f1..4ebc1f0 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
 
 package androidx.credentials.provider
 
@@ -31,7 +32,9 @@
 import androidx.credentials.CredentialOption
 import androidx.credentials.PublicKeyCredential
 import androidx.credentials.R
+import androidx.credentials.provider.PublicKeyCredentialEntry.Api28Impl.toSlice
 import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice
+import androidx.credentials.provider.utils.requiresSlicePropertiesWorkaround
 import java.time.Instant
 import java.util.Collections
 
@@ -67,10 +70,14 @@
  *   this entry was created allows this entry to be auto-selected
  * @property hasDefaultIcon whether this entry was created without a custom icon and hence contains
  *   a default icon set by the library, only to be used in Android API levels >= 28
+ * @property biometricPromptData the data that is set optionally to utilize a credential manager
+ *   flow that directly handles the biometric verification and presents back the response; set to
+ *   null by default, so if not opted in, the embedded biometric prompt flow will not show
  * @throws IllegalArgumentException If [username] is empty
  * @see CredentialEntry
  */
-@RequiresApi(26)
+@RequiresApi(23)
+@Suppress("DEPRECATION") // For usage of slice
 class PublicKeyCredentialEntry
 internal constructor(
     val username: CharSequence,
@@ -84,6 +91,7 @@
     isDefaultIconPreferredAsSingleProvider: Boolean,
     entryGroupId: CharSequence? = username,
     affiliatedDomain: CharSequence? = null,
+    biometricPromptData: BiometricPromptData? = null,
     autoSelectAllowedFromOption: Boolean =
         CredentialOption.extractAutoSelectValue(
             beginGetPublicKeyCredentialOption.candidateQueryData
@@ -92,14 +100,14 @@
     private val isDefaultIconFromSlice: Boolean = false,
 ) :
     CredentialEntry(
-        PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-        beginGetPublicKeyCredentialOption,
-        entryGroupId ?: username,
+        type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+        beginGetCredentialOption = beginGetPublicKeyCredentialOption,
+        entryGroupId = entryGroupId ?: username,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
+        biometricPromptData = biometricPromptData,
     ) {
     val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
-
     @get:JvmName("hasDefaultIcon")
     val hasDefaultIcon: Boolean
         get() {
@@ -159,7 +167,61 @@
         lastUsedTime,
         isAutoSelectAllowed,
         beginGetPublicKeyCredentialOption,
-        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
+        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+    )
+
+    /**
+     * @param context the context of the calling app, required to retrieve fallback resources
+     * @param username the username of the account holding the public key credential
+     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
+     *   entry, must be created with a unique request code per entry, with flag
+     *   [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the final request, and
+     *   NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple times
+     * @param beginGetPublicKeyCredentialOption the option from the original
+     *   [BeginGetCredentialRequest], for which this credential entry is being added
+     * @param displayName the displayName of the account holding the public key credential
+     * @param lastUsedTime the last used time the credential underlying this entry was used by the
+     *   user, distinguishable up to the milli second mark only such that if two entries have the
+     *   same millisecond precision, they will be considered to have been used at the same time
+     * @param icon the icon to be displayed with this entry on the selector, if not set, a default
+     *   icon representing a public key credential type is set by the library
+     * @param isAutoSelectAllowed whether this entry is allowed to be auto selected if it is the
+     *   only one on the UI, only takes effect if the app requesting for credentials also opts for
+     *   auto select
+     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
+     *   default credential type icon (see the default value of [icon]) when you are the only
+     *   available provider; false by default
+     * @param biometricPromptData the data that is set optionally to utilize a credential manager
+     *   flow that directly handles the biometric verification and presents back the response; set
+     *   to null by default, so if not opted in, the embedded biometric prompt flow will not show
+     * @constructor constructs an instance of [PublicKeyCredentialEntry]
+     * @throws NullPointerException If [context], [username], [pendingIntent], or
+     *   [beginGetPublicKeyCredentialOption] is null
+     * @throws IllegalArgumentException if [username] is empty
+     */
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    constructor(
+        context: Context,
+        username: CharSequence,
+        pendingIntent: PendingIntent,
+        beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,
+        displayName: CharSequence? = null,
+        lastUsedTime: Instant? = null,
+        icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
+        isAutoSelectAllowed: Boolean = false,
+        isDefaultIconPreferredAsSingleProvider: Boolean = false,
+        biometricPromptData: BiometricPromptData? = null,
+    ) : this(
+        username,
+        displayName,
+        context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
+        pendingIntent,
+        icon,
+        lastUsedTime,
+        isAutoSelectAllowed,
+        beginGetPublicKeyCredentialOption,
+        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+        biometricPromptData = biometricPromptData,
     )
 
     /**
@@ -186,12 +248,12 @@
      * @throws IllegalArgumentException if [username] is empty
      */
     @Deprecated(
-        "Use the constructor that allows setting all parameters.",
+        "Use the constructor with all parameters dependent on API levels",
         replaceWith =
             ReplaceWith(
                 "PublicKeyCredentialEntry(context, username, pendingIntent," +
                     "beginGetPublicKeyCredentialOption, displayName, lastUsedTime, icon, " +
-                    "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider)"
+                    "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider, biometricPromptData)"
             ),
         level = DeprecationLevel.HIDDEN
     )
@@ -205,20 +267,20 @@
         icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
         isAutoSelectAllowed: Boolean = false,
     ) : this(
-        username,
-        displayName,
-        context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
-        pendingIntent,
-        icon,
-        lastUsedTime,
-        isAutoSelectAllowed,
-        beginGetPublicKeyCredentialOption,
+        username = username,
+        displayName = displayName,
+        typeDisplayName =
+            context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL),
+        pendingIntent = pendingIntent,
+        icon = icon,
+        lastUsedTime = lastUsedTime,
+        isAutoSelectAllowed = isAutoSelectAllowed,
+        beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
         isDefaultIconPreferredAsSingleProvider = false
     )
 
     @RequiresApi(34)
     private object Api34Impl {
-
         @JvmStatic
         fun fromCredentialEntry(
             credentialEntry: android.service.credentials.CredentialEntry
@@ -228,6 +290,95 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private object Api35Impl {
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmStatic
+        fun toSlice(entry: PublicKeyCredentialEntry): Slice {
+            val type = entry.type
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            Api28Impl.addToSlice(entry, sliceBuilder)
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Given multiple API dependencies, this captures common builds across all API levels > V
+        // and across all subclasses for the toSlice method
+        fun addToSlice(entry: PublicKeyCredentialEntry, sliceBuilder: Slice.Builder) {
+            val biometricPromptData = entry.biometricPromptData
+            if (biometricPromptData != null) {
+                if (requiresSlicePropertiesWorkaround()) {
+                    sliceBuilder.addInt(
+                        biometricPromptData.allowedAuthenticators,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
+                    )
+                    biometricPromptData.cryptoObject?.let {
+                        sliceBuilder.addLong(
+                            biometricPromptData.cryptoObject.operationHandle,
+                            /*subType=*/ null,
+                            listOf(SLICE_HINT_CRYPTO_OP_ID)
+                        )
+                    }
+                } else {
+                    val biometricBundle = BiometricPromptData.toBundle(biometricPromptData)
+                    sliceBuilder.addBundle(
+                        biometricBundle,
+                        /*subType=*/ null,
+                        listOf(SLICE_HINT_BIOMETRIC_PROMPT_DATA)
+                    )
+                }
+            }
+        }
+
+        /**
+         * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
+            val publicKeyCredentialEntry = Api28Impl.fromSlice(slice) ?: return null
+            var biometricPromptDataBundle: Bundle? = null
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_BIOMETRIC_PROMPT_DATA)) {
+                    biometricPromptDataBundle = it.bundle
+                }
+            }
+            return try {
+                PublicKeyCredentialEntry(
+                    username = publicKeyCredentialEntry.username,
+                    displayName = publicKeyCredentialEntry.displayName,
+                    typeDisplayName = publicKeyCredentialEntry.typeDisplayName,
+                    pendingIntent = publicKeyCredentialEntry.pendingIntent,
+                    icon = publicKeyCredentialEntry.icon,
+                    lastUsedTime = publicKeyCredentialEntry.lastUsedTime,
+                    isAutoSelectAllowed = publicKeyCredentialEntry.isAutoSelectAllowed,
+                    beginGetPublicKeyCredentialOption =
+                        publicKeyCredentialEntry.beginGetCredentialOption
+                            as BeginGetPublicKeyCredentialOption,
+                    entryGroupId = publicKeyCredentialEntry.entryGroupId,
+                    isDefaultIconPreferredAsSingleProvider =
+                        publicKeyCredentialEntry.isDefaultIconPreferredAsSingleProvider,
+                    affiliatedDomain = publicKeyCredentialEntry.affiliatedDomain,
+                    autoSelectAllowedFromOption =
+                        publicKeyCredentialEntry.isAutoSelectAllowedFromOption,
+                    isCreatedFromSlice = true,
+                    isDefaultIconFromSlice = publicKeyCredentialEntry.isDefaultIconFromSlice,
+                    biometricPromptData =
+                        if (biometricPromptDataBundle != null)
+                            BiometricPromptData.fromBundle(biometricPromptDataBundle!!)
+                        else null
+                )
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+    }
+
     @RequiresApi(28)
     private object Api28Impl {
         @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -244,66 +395,60 @@
         @JvmStatic
         fun toSlice(entry: PublicKeyCredentialEntry): Slice {
             val type = entry.type
-            val title = entry.username
-            val subTitle = entry.displayName
-            val pendingIntent = entry.pendingIntent
-            val typeDisplayName = entry.typeDisplayName
-            val lastUsedTime = entry.lastUsedTime
-            val icon = entry.icon
-            val isAutoSelectAllowed = entry.isAutoSelectAllowed
-            val beginGetPublicKeyCredentialOption = entry.beginGetCredentialOption
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
+            addToSlice(entry, sliceBuilder)
+            return sliceBuilder.build()
+        }
+
+        // Specific to only this custom credential entry, but shared across API levels > P
+        fun addToSlice(entry: PublicKeyCredentialEntry, sliceBuilder: Slice.Builder) {
+            val beginGetCredentialOption = entry.beginGetCredentialOption
             val entryGroupId = entry.entryGroupId
-            val affiliatedDomain = entry.affiliatedDomain
             val isDefaultIconPreferredAsSingleProvider =
                 entry.isDefaultIconPreferredAsSingleProvider
-
-            val autoSelectAllowed =
-                if (isAutoSelectAllowed) {
-                    TRUE_STRING
-                } else {
-                    FALSE_STRING
-                }
+            val affiliatedDomain = entry.affiliatedDomain
             val isUsingDefaultIcon =
                 if (isDefaultIconPreferredAsSingleProvider) {
                     TRUE_STRING
                 } else {
                     FALSE_STRING
                 }
-            val sliceBuilder =
-                Slice.Builder(Uri.EMPTY, SliceSpec(type, REVISION_ID))
-                    .addText(
-                        typeDisplayName,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
-                    )
-                    .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
-                    .addText(subTitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
-                    .addText(
-                        autoSelectAllowed,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_AUTO_ALLOWED)
-                    )
-                    .addText(
-                        beginGetPublicKeyCredentialOption.id,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_OPTION_ID)
-                    )
-                    .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
-                    .addText(
-                        entryGroupId,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_DEDUPLICATION_ID)
-                    )
-                    .addText(
-                        affiliatedDomain,
-                        /*subTypes=*/ null,
-                        listOf(SLICE_HINT_AFFILIATED_DOMAIN)
-                    )
-                    .addText(
-                        isUsingDefaultIcon,
-                        /*subType=*/ null,
-                        listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
-                    )
+            sliceBuilder
+                .addText(
+                    beginGetCredentialOption.id,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_OPTION_ID)
+                )
+                .addText(entryGroupId, /* subTypes= */ null, listOf(SLICE_HINT_DEDUPLICATION_ID))
+                .addText(
+                    isUsingDefaultIcon,
+                    /*subType=*/ null,
+                    listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
+                )
+                .addText(
+                    affiliatedDomain,
+                    /*subTypes=*/ null,
+                    listOf(SLICE_HINT_AFFILIATED_DOMAIN)
+                )
+            val title = entry.username
+            val subtitle = entry.displayName
+            val pendingIntent = entry.pendingIntent
+            val typeDisplayName = entry.typeDisplayName
+            val lastUsedTime = entry.lastUsedTime
+            val icon = entry.icon
+            val isAutoSelectAllowed = entry.isAutoSelectAllowed
+            val autoSelectAllowed =
+                if (isAutoSelectAllowed) {
+                    TRUE_STRING
+                } else {
+                    FALSE_STRING
+                }
+            sliceBuilder
+                .addText(typeDisplayName, /* subType= */ null, listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+                .addText(title, /* subType= */ null, listOf(SLICE_HINT_TITLE))
+                .addText(subtitle, /* subType= */ null, listOf(SLICE_HINT_SUBTITLE))
+                .addText(autoSelectAllowed, /* subType= */ null, listOf(SLICE_HINT_AUTO_ALLOWED))
+                .addIcon(icon, /* subType= */ null, listOf(SLICE_HINT_ICON))
             try {
                 if (entry.hasDefaultIcon) {
                     sliceBuilder.addInt(
@@ -313,7 +458,6 @@
                     )
                 }
             } catch (_: IllegalStateException) {}
-
             if (entry.isAutoSelectAllowedFromOption) {
                 sliceBuilder.addInt(
                     /*true=*/ 1,
@@ -335,7 +479,6 @@
                     .build(),
                 /*subType=*/ null
             )
-            return sliceBuilder.build()
         }
 
         /**
@@ -347,6 +490,10 @@
         @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
         @JvmStatic
         fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
+            var entryGroupId: CharSequence? = null
+            var affiliatedDomain: CharSequence? = null
+            var isDefaultIconPreferredAsSingleProvider = false
+            var beginGetCredentialOptionId: CharSequence? = null
             var typeDisplayName: CharSequence? = null
             var title: CharSequence? = null
             var subtitle: CharSequence? = null
@@ -354,15 +501,21 @@
             var pendingIntent: PendingIntent? = null
             var lastUsedTime: Instant? = null
             var autoSelectAllowed = false
-            var beginGetPublicKeyCredentialOptionId: CharSequence? = null
             var autoSelectAllowedFromOption = false
-            var isDefaultIconPreferredAsSingleProvider = false
             var isDefaultIcon = false
-            var entryGroupId: CharSequence? = null
-            var affiliatedDomain: CharSequence? = null
-
             slice.items.forEach {
-                if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+                if (it.hasHint(SLICE_HINT_OPTION_ID)) {
+                    beginGetCredentialOptionId = it.text
+                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
+                    entryGroupId = it.text
+                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
+                    val defaultIconValue = it.text
+                    if (defaultIconValue == TRUE_STRING) {
+                        isDefaultIconPreferredAsSingleProvider = true
+                    }
+                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
+                    affiliatedDomain = it.text
+                } else if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
                     typeDisplayName = it.text
                 } else if (it.hasHint(SLICE_HINT_TITLE)) {
                     title = it.text
@@ -372,8 +525,6 @@
                     icon = it.icon
                 } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
                     pendingIntent = it.action
-                } else if (it.hasHint(SLICE_HINT_OPTION_ID)) {
-                    beginGetPublicKeyCredentialOptionId = it.text
                 } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
                     lastUsedTime = Instant.ofEpochMilli(it.long)
                 } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
@@ -383,20 +534,10 @@
                     }
                 } else if (it.hasHint(SLICE_HINT_AUTO_SELECT_FROM_OPTION)) {
                     autoSelectAllowedFromOption = true
-                } else if (it.hasHint(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)) {
-                    val defaultIconValue = it.text
-                    if (defaultIconValue == TRUE_STRING) {
-                        isDefaultIconPreferredAsSingleProvider = true
-                    }
                 } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
                     isDefaultIcon = true
-                } else if (it.hasHint(SLICE_HINT_DEDUPLICATION_ID)) {
-                    entryGroupId = it.text
-                } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
-                    affiliatedDomain = it.text
                 }
             }
-
             return try {
                 PublicKeyCredentialEntry(
                     username = title!!,
@@ -409,7 +550,7 @@
                     beginGetPublicKeyCredentialOption =
                         BeginGetPublicKeyCredentialOption.createFromEntrySlice(
                             Bundle(),
-                            beginGetPublicKeyCredentialOptionId!!.toString(),
+                            beginGetCredentialOptionId!!.toString(),
                         ),
                     entryGroupId = entryGroupId,
                     isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
@@ -428,51 +569,6 @@
     companion object {
         private const val TAG = "PublicKeyCredEntry"
 
-        private const val SLICE_HINT_TYPE_DISPLAY_NAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_TITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-
-        private const val SLICE_HINT_SUBTITLE =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-
-        private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-
-        private const val SLICE_HINT_ICON =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-
-        private const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-
-        private const val SLICE_HINT_AUTO_ALLOWED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-
-        private const val SLICE_HINT_IS_DEFAULT_ICON_PREFERRED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_IS_DEFAULT_ICON_PREFERRED"
-
-        private const val SLICE_HINT_OPTION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_OPTION_ID"
-
-        private const val SLICE_HINT_AUTO_SELECT_FROM_OPTION =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_SELECT_FROM_OPTION"
-
-        private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
-
-        private const val SLICE_HINT_AFFILIATED_DOMAIN =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
-
-        private const val SLICE_HINT_DEDUPLICATION_ID =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
-
-        private const val TRUE_STRING = "true"
-
-        private const val FALSE_STRING = "false"
-
-        private const val REVISION_ID = 1
-
         /**
          * Converts an instance of [PublicKeyCredentialEntry] to a [Slice].
          *
@@ -482,14 +578,16 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @JvmStatic
         fun toSlice(entry: PublicKeyCredentialEntry): Slice? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.toSlice(entry)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.toSlice(entry)
             }
             return null
         }
 
         /**
-         * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
+         * Returns an instance of [PublicKeyCredentialEntry] derived from a [Slice] object.
          *
          * @param slice the [Slice] object constructed through [toSlice]
          */
@@ -497,7 +595,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @JvmStatic
         fun fromSlice(slice: Slice): PublicKeyCredentialEntry? {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 35) {
+                return Api35Impl.fromSlice(slice)
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 return Api28Impl.fromSlice(slice)
             }
             return null
@@ -536,6 +636,7 @@
         private var icon: Icon? = null
         private var autoSelectAllowed: Boolean = false
         private var isDefaultIconPreferredAsSingleProvider: Boolean = false
+        private var biometricPromptData: BiometricPromptData? = null
 
         /** Sets a displayName to be shown on the UI with this entry */
         fun setDisplayName(displayName: CharSequence?): Builder {
@@ -549,6 +650,18 @@
             return this
         }
 
+        /**
+         * Sets the biometric prompt data to optionally utilize a credential manager flow that
+         * directly handles the biometric verification for you and gives you the response; set to
+         * null by default, indicating the default behavior is to not utilize this embedded
+         * biometric prompt flow.
+         */
+        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
+            this.biometricPromptData = biometricPromptData
+            return this
+        }
+
         /** Sets whether the entry should be auto-selected. The value is false by default */
         @Suppress("MissingGetterMatchingBuilder")
         fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
@@ -585,15 +698,16 @@
             val typeDisplayName =
                 context.getString(R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL)
             return PublicKeyCredentialEntry(
-                username,
-                displayName,
-                typeDisplayName,
-                pendingIntent,
-                icon!!,
-                lastUsedTime,
-                autoSelectAllowed,
-                beginGetPublicKeyCredentialOption,
-                isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
+                username = username,
+                displayName = displayName,
+                typeDisplayName = typeDisplayName,
+                pendingIntent = pendingIntent,
+                icon = icon!!,
+                lastUsedTime = lastUsedTime,
+                isAutoSelectAllowed = autoSelectAllowed,
+                beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
+                isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
+                biometricPromptData = biometricPromptData,
             )
         }
     }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
index 1ee7c00..d542e03 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:Suppress("deprecation") // For usage of Slice
+
 package androidx.credentials.provider
 
 import android.annotation.SuppressLint
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt
new file mode 100644
index 0000000..53417f2
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/utils/EntryUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.credentials.provider.utils
+
+import android.os.Build
+
+internal const val ANDROID_15_BETA_3 = "AP31.240517.022"
+internal const val ANDROID_15_BETA_2_2 = "AP31.240426.023.B4"
+internal const val ANDROID_15_BETA_2_1 = "AP31.240426.023"
+internal const val ANDROID_15_BETA_2 = "AP31.240426.022"
+
+internal val buildsUsingSliceProperties =
+    setOf(ANDROID_15_BETA_2, ANDROID_15_BETA_2_1, ANDROID_15_BETA_2_2, ANDROID_15_BETA_3)
+
+/**
+ * The library owners aim to support early partners across beta2 and beta3 devices for Android 15,
+ * requiring this to be introduced for backwards compatibility. Beyond this temporary use case, the
+ * library owners aim to no longer utilize this functionality.
+ */
+internal fun requiresSlicePropertiesWorkaround(): Boolean =
+    buildsUsingSliceProperties.contains(Build.ID)
diff --git a/credentials/credentials/src/main/res/values-hy/strings.xml b/credentials/credentials/src/main/res/values-hy/strings.xml
index 617300a0..2dfb774f 100644
--- a/credentials/credentials/src/main/res/values-hy/strings.xml
+++ b/credentials/credentials/src/main/res/values-hy/strings.xml
@@ -17,6 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Անցաբառ"</string>
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Մուտքի բանալի"</string>
     <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Գաղտնաբառ"</string>
 </resources>
diff --git a/credentials/credentials/src/main/res/values-nb/strings.xml b/credentials/credentials/src/main/res/values-nb/strings.xml
index a72318a..9eb70fe 100644
--- a/credentials/credentials/src/main/res/values-nb/strings.xml
+++ b/credentials/credentials/src/main/res/values-nb/strings.xml
@@ -17,6 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Tilgangsnøkkel"</string>
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passnøkkel"</string>
     <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passord"</string>
 </resources>
diff --git a/credentials/credentials/src/main/res/values-tr/strings.xml b/credentials/credentials/src/main/res/values-tr/strings.xml
index f00b298..02256b8 100644
--- a/credentials/credentials/src/main/res/values-tr/strings.xml
+++ b/credentials/credentials/src/main/res/values-tr/strings.xml
@@ -17,6 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Şifre anahtarı"</string>
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Geçiş anahtarı"</string>
     <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Şifre"</string>
 </resources>
diff --git a/credentials/credentials/src/main/res/values-uz/strings.xml b/credentials/credentials/src/main/res/values-uz/strings.xml
index 7f1bb8c..77100e0 100644
--- a/credentials/credentials/src/main/res/values-uz/strings.xml
+++ b/credentials/credentials/src/main/res/values-uz/strings.xml
@@ -17,6 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kod"</string>
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kirish kaliti"</string>
     <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
 </resources>
diff --git a/credentials/credentials/src/main/res/values/ids.xml b/credentials/credentials/src/main/res/values/ids.xml
new file mode 100644
index 0000000..3aac762
--- /dev/null
+++ b/credentials/credentials/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="androidx_credential_pendingCredentialRequest" type="id"/>
+</resources>
\ No newline at end of file
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 2e5f736..07d5fe6 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -54,6 +54,7 @@
     implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
 }
 android {
+    compileSdk 35
     namespace 'com.example.datastorecomposesamples'
     defaultConfig {
         minSdk 28
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index 6f9146c..3cabbbf 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.
@@ -194,4 +194,4 @@
 #-----------------------------------------------------------------------
 # Enable compose @Preview rendering
 #-----------------------------------------------------------------------
-compose.project.uses.compose.override=true
+compose.project.uses.compose.override=true
\ No newline at end of file
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 897a4ac..bb7a6a4 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -21,6 +21,7 @@
     samples("androidx.window:window-samples:1.3.0-alpha03")
 
     docsForOptionalProject(":xr:xr")
+    docsForOptionalProject(":xr:xr-material3-adaptive")
     docs(project(":activity:activity"))
     docs(project(":activity:activity-compose"))
     docs(project(":activity:activity-ktx"))
diff --git a/emoji2/emoji2-emojipicker/samples/build.gradle b/emoji2/emoji2-emojipicker/samples/build.gradle
index 6824517..3af2482 100644
--- a/emoji2/emoji2-emojipicker/samples/build.gradle
+++ b/emoji2/emoji2-emojipicker/samples/build.gradle
@@ -34,6 +34,7 @@
     implementation(project(":compose:foundation:foundation"))
 }
 android {
+    compileSdk 35
     namespace "androidx.emoji2.emojipicker.samples"
 }
 
diff --git a/gradle.properties b/gradle.properties
index 6edafda..6cd68fb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,19 +32,19 @@
 # Remove when AGP defaults to 2.1.0
 android.prefabVersion=2.1.0
 
-# Do generate versioned API files
-androidx.writeVersionedApiFiles=true
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
 
-# Do run the CheckAarMetadata task
-android.experimental.disableCompileSdkChecks=false
+# Don't run the CheckAarMetadata task
+android.experimental.disableCompileSdkChecks=true
 
 # Don't warn about needing to update AGP
-android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33,34
+android.suppressUnsupportedCompileSdk=35
 
 androidx.compileSdk=34
 androidx.targetSdkVersion=34
 androidx.allowCustomCompileSdk=true
-androidx.includeOptionalProjects=false
+androidx.includeOptionalProjects=true
 
 # Disable features we do not use
 android.defaults.buildfeatures.aidl=false
diff --git a/hilt/integration-tests/viewmodelapp/build.gradle b/hilt/integration-tests/viewmodelapp/build.gradle
index 0db4203..12eec0a 100644
--- a/hilt/integration-tests/viewmodelapp/build.gradle
+++ b/hilt/integration-tests/viewmodelapp/build.gradle
@@ -23,6 +23,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         testInstrumentationRunner "androidx.hilt.integration.viewmodelapp.TestRunner"
     }
diff --git a/hilt/integration-tests/workerapp/build.gradle b/hilt/integration-tests/workerapp/build.gradle
index 391f756..b277151 100644
--- a/hilt/integration-tests/workerapp/build.gradle
+++ b/hilt/integration-tests/workerapp/build.gradle
@@ -23,6 +23,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         testInstrumentationRunner "androidx.hilt.integration.workerapp.TestRunner"
     }
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 8083aae..9a6f0d9 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -71,7 +71,8 @@
 }
 
 android {
-  namespace = "androidx.ink.brush"
+    compileSdk 35
+    namespace = "androidx.ink.brush"
 }
 
 androidx {
diff --git a/libraryversions.toml b/libraryversions.toml
index a6f57a1..fd09be2 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -8,7 +8,7 @@
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.3.0-alpha02"
 BENCHMARK = "1.3.0-rc01"
-BIOMETRIC = "1.2.0-alpha06"
+BIOMETRIC = "1.4.0-alpha01"
 BLUETOOTH = "1.0.0-alpha02"
 BROWSER = "1.9.0-alpha01"
 BUILDSRC_TESTS = "1.0.0-alpha01"
@@ -30,7 +30,7 @@
 CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
 CONTENTPAGER = "1.1.0-alpha01"
 COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.14.0-alpha01"
+CORE = "1.15.0-alpha01"
 CORE_ANIMATION = "1.0.0"
 CORE_ANIMATION_TESTING = "1.0.0"
 CORE_APPDIGEST = "1.0.0-alpha01"
@@ -44,7 +44,7 @@
 CORE_SPLASHSCREEN = "1.2.0-alpha01"
 CORE_TELECOM = "1.0.0-alpha10"
 CORE_UWB = "1.0.0-alpha08"
-CREDENTIALS = "1.3.0-rc01"
+CREDENTIALS = "1.5.0-alpha03"
 CREDENTIALS_E2EE_QUARANTINE = "1.0.0-alpha02"
 CREDENTIALS_FIDO_QUARANTINE = "1.0.0-alpha02"
 CURSORADAPTER = "1.1.0-alpha01"
@@ -116,7 +116,7 @@
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.7.0-alpha06"
+ROOM = "2.7.0-alpha05"
 SAFEPARCEL = "1.0.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha07"
@@ -132,7 +132,7 @@
 SLICE_BUILDERS_KTX = "1.0.0-alpha09"
 SLICE_REMOTECALLBACK = "1.0.0-alpha01"
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.5.0-alpha06"
+SQLITE = "2.5.0-alpha05"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
 STABLE_AIDL = "1.0.0-alpha01"
 STARTUP = "1.2.0-alpha03"
@@ -169,8 +169,8 @@
 WEAR_WATCHFACE = "1.3.0-alpha03"
 WEBKIT = "1.12.0-alpha02"
 # Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.4.0-alpha01"
-WINDOW_EXTENSIONS = "1.4.0-alpha01"
+WINDOW = "1.6.0-alpha01"
+WINDOW_EXTENSIONS = "1.6.0-alpha01"
 WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
 WINDOW_SIDECAR = "1.0.0-rc01"
 WORK = "2.10.0-alpha02"
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 703f61c..80ec967 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -80,6 +80,8 @@
 }
 
 android {
+    compileSdk 35
+    
     buildTypes.configureEach {
         consumerProguardFiles "proguard-rules.pro"
     }
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 05e15d6..db0b559 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -99,5 +99,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.lifecycle.viewmodel.compose"
 }
diff --git a/loader/loader-ktx/build.gradle b/loader/loader-ktx/build.gradle
index c700ba7..cf8f944f 100644
--- a/loader/loader-ktx/build.gradle
+++ b/loader/loader-ktx/build.gradle
@@ -56,5 +56,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.loader.ktx"
 }
diff --git a/loader/loader/build.gradle b/loader/loader/build.gradle
index 5e08078..cf1b2a0 100644
--- a/loader/loader/build.gradle
+++ b/loader/loader/build.gradle
@@ -38,5 +38,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.loader"
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
index ec6a61b..1ab6667 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
@@ -596,4 +596,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
index a12969f..e88cc01 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -162,6 +162,7 @@
             ApplicationInfo appInfo = activityInfo.applicationInfo;
             if (((ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
                     & appInfo.flags) != 0) {
+                intent.setPackage(appInfo.packageName);
                 context.startActivity(intent);
                 return true;
             }
@@ -191,6 +192,7 @@
             ApplicationInfo appInfo = activityInfo.applicationInfo;
             if (((ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)
                     & appInfo.flags) != 0) {
+                intent.setPackage(appInfo.packageName);
                 context.startActivity(intent);
                 return true;
             }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index f4a25ae..201dd205 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -404,6 +404,13 @@
 
     /* package */ void selectRoute(
             @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
+        selectRoute(route, unselectReason, /* syncMediaRoute1Provider= */ true);
+    }
+
+    /* package */ void selectRoute(
+            @NonNull MediaRouter.RouteInfo route,
+            @MediaRouter.UnselectReason int unselectReason,
+            boolean syncMediaRoute1Provider) {
         if (!mRoutes.contains(route)) {
             Log.w(TAG, "Ignoring attempt to select removed route: " + route);
             return;
@@ -420,7 +427,7 @@
                 && mSelectedRoute != route) {
             mMr2Provider.transferTo(route.getDescriptorId());
         } else {
-            selectRouteInternal(route, unselectReason);
+            selectRouteInternal(route, unselectReason, syncMediaRoute1Provider);
         }
     }
 
@@ -922,7 +929,15 @@
                     "Unselecting the current route because it "
                             + "is no longer selectable: "
                             + mSelectedRoute);
-            selectRouteInternal(chooseFallbackRoute(), UNSELECT_REASON_UNKNOWN);
+            // TODO: b/294968421 - Consider passing a false syncMediaRoute1Provider. This could help
+            // with the prevention of setBluetoothA2dpOn(false) bugs, but it could also leave the
+            // platform MediaRouter in an inconsistent state. In order to change
+            // syncMediaRoute1Provider to false, we need to assess the impact of not calling
+            // android.media.MediaRouter.selectRoute as a result of this method call.
+            selectRouteInternal(
+                    chooseFallbackRoute(),
+                    UNSELECT_REASON_UNKNOWN,
+                    /* syncMediaRoute1Provider= */ true);
         } else if (selectedRouteDescriptorChanged) {
             // In case the selected route is a route group, select/unselect route controllers
             // for the added/removed route members.
@@ -958,7 +973,9 @@
     }
 
     /* package */ void selectRouteInternal(
-            @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
+            @NonNull MediaRouter.RouteInfo route,
+            @MediaRouter.UnselectReason int unselectReason,
+            boolean syncMediaRoute1Provider) {
         if (mSelectedRoute == route) {
             return;
         }
@@ -1049,16 +1066,18 @@
         if (mSelectedRoute == null) {
             mSelectedRoute = route;
             mSelectedRouteController = routeController;
-            mCallbackHandler.post(
-                    GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
-                    new Pair<>(null, route),
-                    unselectReason);
+            mCallbackHandler.postRouteSelectedMessage(
+                    /* fromRoute= */ null,
+                    /* targetRoute= */ route,
+                    unselectReason,
+                    syncMediaRoute1Provider);
         } else {
             notifyTransfer(
                     this,
                     route,
                     routeController,
                     unselectReason,
+                    syncMediaRoute1Provider,
                     /* requestedRoute= */ null,
                     /* memberRoutes= */ null);
         }
@@ -1106,19 +1125,26 @@
             MediaRouter.RouteInfo route,
             @Nullable MediaRouteProvider.RouteController routeController,
             @MediaRouter.UnselectReason int reason,
+            boolean syncMediaRoute1Provider,
             @Nullable MediaRouter.RouteInfo requestedRoute,
             @Nullable
-            Collection<
-                    MediaRouteProvider.DynamicGroupRouteController
-                            .DynamicRouteDescriptor>
-                    memberRoutes) {
+                    Collection<
+                                    MediaRouteProvider.DynamicGroupRouteController
+                                            .DynamicRouteDescriptor>
+                            memberRoutes) {
         if (mTransferNotifier != null) {
             mTransferNotifier.cancel();
             mTransferNotifier = null;
         }
         mTransferNotifier =
                 new MediaRouter.PrepareTransferNotifier(
-                        router, route, routeController, reason, requestedRoute, memberRoutes);
+                        router,
+                        route,
+                        routeController,
+                        reason,
+                        syncMediaRoute1Provider,
+                        requestedRoute,
+                        memberRoutes);
 
         if (mTransferNotifier.mReason != UNSELECT_REASON_ROUTE_CHANGED
                 || mOnPrepareTransferListener == null) {
@@ -1137,51 +1163,52 @@
 
     /* package */ MediaRouteProvider.DynamicGroupRouteController.OnDynamicRoutesChangedListener
             mDynamicRoutesListener =
-            new MediaRouteProvider.DynamicGroupRouteController
-                    .OnDynamicRoutesChangedListener() {
-                @Override
-                public void onRoutesChanged(
-                        @NonNull MediaRouteProvider.DynamicGroupRouteController controller,
-                        @Nullable MediaRouteDescriptor groupRouteDescriptor,
-                        @NonNull
-                        Collection<
-                                MediaRouteProvider
-                                        .DynamicGroupRouteController
-                                        .DynamicRouteDescriptor>
-                                routes) {
-                    if (controller == mRequestedRouteController
-                            && groupRouteDescriptor != null) {
-                        MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
-                        String groupId = groupRouteDescriptor.getId();
+                    new MediaRouteProvider.DynamicGroupRouteController
+                            .OnDynamicRoutesChangedListener() {
+                        @Override
+                        public void onRoutesChanged(
+                                @NonNull MediaRouteProvider.DynamicGroupRouteController controller,
+                                @Nullable MediaRouteDescriptor groupRouteDescriptor,
+                                @NonNull
+                                        Collection<
+                                                        MediaRouteProvider
+                                                                .DynamicGroupRouteController
+                                                                .DynamicRouteDescriptor>
+                                                routes) {
+                            if (controller == mRequestedRouteController
+                                    && groupRouteDescriptor != null) {
+                                MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
+                                String groupId = groupRouteDescriptor.getId();
 
-                        String uniqueId = assignRouteUniqueId(provider, groupId);
-                        MediaRouter.RouteInfo route =
-                                new MediaRouter.RouteInfo(provider, groupId, uniqueId);
-                        route.maybeUpdateDescriptor(groupRouteDescriptor);
+                                String uniqueId = assignRouteUniqueId(provider, groupId);
+                                MediaRouter.RouteInfo route =
+                                        new MediaRouter.RouteInfo(provider, groupId, uniqueId);
+                                route.maybeUpdateDescriptor(groupRouteDescriptor);
 
-                        if (mSelectedRoute == route) {
-                            return;
+                                if (mSelectedRoute == route) {
+                                    return;
+                                }
+
+                                notifyTransfer(
+                                        GlobalMediaRouter.this,
+                                        route,
+                                        mRequestedRouteController,
+                                        UNSELECT_REASON_ROUTE_CHANGED,
+                                        /* syncMediaRoute1Provider= */ true,
+                                        mRequestedRoute,
+                                        routes);
+
+                                mRequestedRoute = null;
+                                mRequestedRouteController = null;
+                            } else if (controller == mSelectedRouteController) {
+                                if (groupRouteDescriptor != null) {
+                                    updateRouteDescriptorAndNotify(
+                                            mSelectedRoute, groupRouteDescriptor);
+                                }
+                                mSelectedRoute.updateDynamicDescriptors(routes);
+                            }
                         }
-
-                        notifyTransfer(
-                                GlobalMediaRouter.this,
-                                route,
-                                mRequestedRouteController,
-                                UNSELECT_REASON_ROUTE_CHANGED,
-                                mRequestedRoute,
-                                routes);
-
-                        mRequestedRoute = null;
-                        mRequestedRouteController = null;
-                    } else if (controller == mSelectedRouteController) {
-                        if (groupRouteDescriptor != null) {
-                            updateRouteDescriptorAndNotify(
-                                    mSelectedRoute, groupRouteDescriptor);
-                        }
-                        mSelectedRoute.updateDynamicDescriptors(routes);
-                    }
-                }
-            };
+                    };
 
     @Override
     public void onPlatformRouteSelectedByDescriptorId(@NonNull String id) {
@@ -1329,7 +1356,12 @@
                 return;
             }
 
-            selectRouteInternal(routeToSelect, reason);
+            // TODO: b/294968421 - Consider passing a false syncMediaRoute1Provider. This could help
+            // with the prevention of setBluetoothA2dpOn(false) bugs, but it could also leave the
+            // platform MediaRouter in an inconsistent state. In order to change
+            // syncMediaRoute1Provider to false, we need to assess the impact of not calling
+            // android.media.MediaRouter.selectRoute as a result of this method call.
+            selectRouteInternal(routeToSelect, reason, /* syncMediaRoute1Provider */ true);
         }
 
         @Override
@@ -1356,7 +1388,7 @@
         /* package */ void selectRouteToFallbackRoute(@MediaRouter.UnselectReason int reason) {
             MediaRouter.RouteInfo fallbackRoute = chooseFallbackRoute();
             if (getSelectedRoute() != fallbackRoute) {
-                selectRouteInternal(fallbackRoute, reason);
+                selectRouteInternal(fallbackRoute, reason, /* syncMediaRoute1Provider */ true);
             }
             // Does nothing when the selected route is same with fallback route.
             // This is the difference between this and unselect().
@@ -1497,7 +1529,29 @@
 
         public static final int MSG_ROUTER_PARAMS_CHANGED = MSG_TYPE_ROUTER | 1;
 
-        CallbackHandler() {
+        /* package */ void postRouteSelectedMessage(
+                @Nullable MediaRouter.RouteInfo fromRoute,
+                @NonNull MediaRouter.RouteInfo targetRoute,
+                int reason,
+                boolean syncMediaRoute1Provider) {
+            RouteSelectedMessageParams params =
+                    new RouteSelectedMessageParams(fromRoute, targetRoute, syncMediaRoute1Provider);
+            Message message = obtainMessage(MSG_ROUTE_SELECTED, params);
+            message.arg1 = reason;
+            message.sendToTarget();
+        }
+
+        /* package */ void postAnotherRouteSelectedMessage(
+                @Nullable MediaRouter.RouteInfo requestedRoute,
+                @NonNull MediaRouter.RouteInfo targetRoute,
+                int reason,
+                boolean syncMediaRoute1Provider) {
+            RouteSelectedMessageParams params =
+                    new RouteSelectedMessageParams(
+                            requestedRoute, targetRoute, syncMediaRoute1Provider);
+            Message message = obtainMessage(MSG_ROUTE_ANOTHER_SELECTED, params);
+            message.arg1 = reason;
+            message.sendToTarget();
         }
 
         /* package */ void post(int msg, Object obj) {
@@ -1562,9 +1616,11 @@
                             (MediaRouter.RouteInfo) obj);
                     break;
                 case MSG_ROUTE_SELECTED: {
-                    MediaRouter.RouteInfo selectedRoute =
-                            ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
-                    mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(selectedRoute);
+                    RouteSelectedMessageParams params = (RouteSelectedMessageParams) obj;
+                    MediaRouter.RouteInfo selectedRoute = params.mTargetRoute;
+                    if (params.mSyncMediaRoute1Provider) {
+                        mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(selectedRoute);
+                    }
                     // TODO(b/166794092): Remove this nullness check
                     if (mDefaultRoute != null && selectedRoute.isDefaultOrBluetooth()) {
                         for (MediaRouter.RouteInfo prevGroupRoute : mDynamicGroupRoutes) {
@@ -1575,11 +1631,13 @@
                     break;
                 }
                 case MSG_ROUTE_ANOTHER_SELECTED: {
-                    MediaRouter.RouteInfo groupRoute =
-                            ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
+                    RouteSelectedMessageParams params = (RouteSelectedMessageParams) obj;
+                    MediaRouter.RouteInfo groupRoute = params.mTargetRoute;
                     mDynamicGroupRoutes.add(groupRoute);
                     mPlatformMediaRouter1RouteProvider.onSyncRouteAdded(groupRoute);
-                    mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(groupRoute);
+                    if (params.mSyncMediaRoute1Provider) {
+                        mPlatformMediaRouter1RouteProvider.onSyncRouteSelected(groupRoute);
+                    }
                     break;
                 }
             }
@@ -1592,16 +1650,18 @@
             final MediaRouter.Callback callback = record.mCallback;
             switch (what & MSG_TYPE_MASK) {
                 case MSG_TYPE_ROUTE: {
+                    RouteSelectedMessageParams selectedMessageParams =
+                                what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED
+                                        ? ((RouteSelectedMessageParams) obj)
+                                        : null;
                     final MediaRouter.RouteInfo route =
-                            (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
-                                    ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
-                                    .second
-                                    : (MediaRouter.RouteInfo) obj;
+                                selectedMessageParams != null
+                                        ? selectedMessageParams.mTargetRoute
+                                        : (MediaRouter.RouteInfo) obj;
                     final MediaRouter.RouteInfo optionalRoute =
-                            (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
-                                    ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
-                                    .first
-                                    : null;
+                                selectedMessageParams != null
+                                        ? selectedMessageParams.mFromOrRequestedRoute
+                                        : null;
                     if (route == null
                             || !record.filterRouteEvent(route, what, optionalRoute, arg)) {
                         break;
@@ -1661,4 +1721,29 @@
             }
         }
     }
+
+    /**
+     * Holds the parameters of {@link CallbackHandler#MSG_ROUTE_SELECTED} and {@link
+     * CallbackHandler#MSG_ROUTE_ANOTHER_SELECTED}.
+     */
+    private static final class RouteSelectedMessageParams {
+        /**
+         * Holds the origin route for {@link CallbackHandler#MSG_ROUTE_SELECTED}, or the originally
+         * requested route for {@link CallbackHandler#MSG_ROUTE_ANOTHER_SELECTED}.
+         */
+        @Nullable public final MediaRouter.RouteInfo mFromOrRequestedRoute;
+
+        @NonNull public final MediaRouter.RouteInfo mTargetRoute;
+
+        public final boolean mSyncMediaRoute1Provider;
+
+        private RouteSelectedMessageParams(
+                @Nullable MediaRouter.RouteInfo fromOrRequestedRoute,
+                @NonNull MediaRouter.RouteInfo targetRoute,
+                boolean syncMediaRoute1Provider) {
+            mFromOrRequestedRoute = fromOrRequestedRoute;
+            mTargetRoute = targetRoute;
+            mSyncMediaRoute1Provider = syncMediaRoute1Provider;
+        }
+    }
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index 462b25e..6220936 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -405,7 +405,6 @@
     }
 
     private class TransferCallback extends MediaRouter2.TransferCallback {
-        TransferCallback() {}
 
         @Override
         public void onTransfer(@NonNull MediaRouter2.RoutingController oldController,
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 1a6bad4..faabbdc 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -43,7 +43,6 @@
 import androidx.annotation.RestrictTo;
 import androidx.collection.ArrayMap;
 import androidx.core.util.ObjectsCompat;
-import androidx.core.util.Pair;
 import androidx.mediarouter.app.MediaRouteDiscoveryFragment;
 import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController;
 import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
@@ -1979,11 +1978,29 @@
          */
         @MainThread
         public void select() {
-            checkCallingThread();
-            getGlobalRouter().selectRoute(this, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
+            select(/* syncMediaRoute1Provider= */ true);
         }
 
         /**
+         * Selects this media route.
+         *
+         * @param syncMediaRoute1Provider Whether this selection should be passed through to {@link
+         *     PlatformMediaRouter1RouteProvider}. Should be false when this call is the result of a
+         *     {@link MediaRouter.Callback#onRouteSelected} call.
+         */
+        @RestrictTo(LIBRARY)
+        @MainThread
+        public void select(boolean syncMediaRoute1Provider) {
+            checkCallingThread();
+            getGlobalRouter()
+                    .selectRoute(
+                            this,
+                            MediaRouter.UNSELECT_REASON_ROUTE_CHANGED,
+                            syncMediaRoute1Provider);
+        }
+
+
+        /**
          * Returns true if the route has one or more members
          */
         @RestrictTo(LIBRARY)
@@ -2691,6 +2708,7 @@
 
         final RouteController mToRouteController;
         final @UnselectReason int mReason;
+        private final boolean mSyncMediaRoute1Provider;
         private final RouteInfo mFromRoute;
         final RouteInfo mToRoute;
         private final RouteInfo mRequestedRoute;
@@ -2702,8 +2720,12 @@
         private boolean mFinished = false;
         private boolean mCanceled = false;
 
-        PrepareTransferNotifier(GlobalMediaRouter router, RouteInfo route,
-                @Nullable RouteController routeController, @UnselectReason int reason,
+        PrepareTransferNotifier(
+                GlobalMediaRouter router,
+                RouteInfo route,
+                @Nullable RouteController routeController,
+                @UnselectReason int reason,
+                boolean syncMediaRoute1Provider,
                 @Nullable RouteInfo requestedRoute,
                 @Nullable Collection<DynamicRouteDescriptor> memberRoutes) {
             mRouter = new WeakReference<>(router);
@@ -2711,6 +2733,7 @@
             mToRoute = route;
             mToRouteController = routeController;
             mReason = reason;
+            mSyncMediaRoute1Provider = syncMediaRoute1Provider;
             mFromRoute = router.mSelectedRoute;
             mRequestedRoute = requestedRoute;
             mMemberRoutes = (memberRoutes == null) ? null : new ArrayList<>(memberRoutes);
@@ -2807,12 +2830,11 @@
             router.mSelectedRouteController = mToRouteController;
 
             if (mRequestedRoute == null) {
-                router.mCallbackHandler.post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
-                        new Pair<>(mFromRoute, mToRoute), mReason);
+                router.mCallbackHandler.postRouteSelectedMessage(
+                        mFromRoute, mToRoute, mReason, mSyncMediaRoute1Provider);
             } else {
-                router.mCallbackHandler.post(
-                        GlobalMediaRouter.CallbackHandler.MSG_ROUTE_ANOTHER_SELECTED,
-                        new Pair<>(mRequestedRoute, mToRoute), mReason);
+                router.mCallbackHandler.postAnotherRouteSelectedMessage(
+                        mRequestedRoute, mToRoute, mReason, mSyncMediaRoute1Provider);
             }
 
             router.mRouteControllerMap.clear();
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
index 52c2cce..322240f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
@@ -312,7 +312,7 @@
 
             UserRouteRecord userRouteRecord = getUserRouteRecord(route);
             if (userRouteRecord != null) {
-                userRouteRecord.mRoute.select();
+                userRouteRecord.mRoute.select(/* syncMediaRoute1Provider= */ false);
             } else {
                 // Select the route if it already exists in the compat media router.
                 // If not, we will select it instead when the route is added.
diff --git a/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml b/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
index 4d5ffc5..c75c58e 100644
--- a/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-zh-rCN/strings.xml
@@ -51,8 +51,12 @@
     <string name="mr_chooser_wifi_warning_description_unknown" msgid="3459891599800041449">"确保另一设备与此设备连接到同一 WLAN 网络"</string>
     <string name="mr_chooser_wifi_learn_more" msgid="3799500840179081429"><a href="https://support.google.com/chromecast/?p=trouble-finding-devices">"了解详情"</a></string>
     <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"了解如何投放"</string>
-    <string name="mr_route_name_unknown" msgid="5538521943939635302">"未知"</string>
-    <string name="mr_route_name_bluetooth" msgid="5056346328175584455">"蓝牙"</string>
-    <string name="mr_route_name_tv" msgid="8041420425123528188">"电视"</string>
-    <string name="mr_route_name_speaker" msgid="708574147123374685">"音箱"</string>
+    <!-- no translation found for mr_route_name_unknown (5538521943939635302) -->
+    <skip />
+    <!-- no translation found for mr_route_name_bluetooth (5056346328175584455) -->
+    <skip />
+    <!-- no translation found for mr_route_name_tv (8041420425123528188) -->
+    <skip />
+    <!-- no translation found for mr_route_name_speaker (708574147123374685) -->
+    <skip />
 </resources>
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index da13b35..90a1980 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -24,6 +24,7 @@
     buildFeatures {
         viewBinding true
     }
+    compileSdk 35
     namespace "androidx.metrics.performance.janktest"
 }
 
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 03a6aca..d532d8a 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -50,5 +50,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.navigation.compose.demos"
 }
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b7b3ffb..19e494b 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -59,5 +59,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.navigation.compose.samples"
 }
diff --git a/paging/integration-tests/testapp/build.gradle b/paging/integration-tests/testapp/build.gradle
index 0988655..cbd8d34 100644
--- a/paging/integration-tests/testapp/build.gradle
+++ b/paging/integration-tests/testapp/build.gradle
@@ -55,6 +55,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.paging.integration.testapp"
 }
 
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 020a4f2..1a9503f 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -86,6 +86,7 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.paging.compose"
     // TODO(b/313699418): need to update compose.runtime version to 1.6.0+
     experimentalProperties["android.lint.useK2Uast"] = false
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index e9cb7bb..2541828 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -56,5 +56,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.paging.compose.demos"
 }
diff --git a/paging/paging-compose/samples/build.gradle b/paging/paging-compose/samples/build.gradle
index 1df0616..2980ac1 100644
--- a/paging/paging-compose/samples/build.gradle
+++ b/paging/paging-compose/samples/build.gradle
@@ -48,5 +48,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.paging.compose.samples"
 }
diff --git a/pdf/integration-tests/testapp/build.gradle b/pdf/integration-tests/testapp/build.gradle
index 1b1a2e5..021fa34 100644
--- a/pdf/integration-tests/testapp/build.gradle
+++ b/pdf/integration-tests/testapp/build.gradle
@@ -6,17 +6,17 @@
 
 android {
     namespace 'androidx.pdf.testapp'
+    buildToolsVersion "35.0.0-rc1"
 
     defaultConfig {
         applicationId "androidx.pdf.testapp"
-        minSdkVersion 31
+        minSdk 31
+        compileSdk 35
     }
 }
 
 dependencies {
-
     api("com.google.android.material:material:1.11.0")
     implementation(project(":pdf:pdf-viewer"))
-
     implementation(libs.constraintLayout)
 }
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
index 4a33224..6caf23a 100644
--- a/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/pdf/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -27,10 +27,9 @@
         android:theme="@style/Theme.Androidx"
         tools:replace="android:label">
         <activity
-            android:name=".MainActivity"
+            android:name=".LegacyMainActivity"
             android:exported="true"
-            android:windowSoftInputMode="adjustResize"
-            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
+            android:windowSoftInputMode="adjustResize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
diff --git a/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java b/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java
new file mode 100644
index 0000000..01344f4
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/java/androidx/pdf/testapp/LegacyMainActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.testapp;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.pdf.viewer.PdfViewer;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings({"deprecation", "RestrictedApiAndroidX"})
+public class LegacyMainActivity extends AppCompatActivity {
+
+    private PdfViewer mPdfViewer;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        Button getContentButton = findViewById(R.id.launch_button);
+        assert getContentButton != null;
+        getContentButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.addCategory(Intent.CATEGORY_OPENABLE);
+                intent.setType("application/pdf");
+                startActivityForResult(intent, 2);
+            }
+        });
+    }
+
+    @Override
+    protected void onActivityResult(int requestcode, int resultcode, @Nullable Intent data) {
+        super.onActivityResult(requestcode, resultcode, data);
+        assert data != null;
+        Uri uri = data.getData();
+        if (uri != null) {
+            setPdfViewer();
+            mPdfViewer.loadFile(uri);
+        }
+    }
+
+    void setPdfViewer() {
+
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        FragmentTransaction transaction = fragmentManager.beginTransaction();
+
+        mPdfViewer = new PdfViewer();
+        transaction.replace(R.id.fragment_container_view, mPdfViewer, null);
+        transaction.commitAllowingStateLoss();
+        fragmentManager.executePendingTransactions();
+    }
+}
diff --git a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 7820e52..a8a25f6 100644
--- a/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/pdf/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -23,7 +23,7 @@
     android:layout_gravity="center"
     android:fitsSystemWindows="true"
     android:orientation="vertical"
-    tools:context=".MainActivity">
+    tools:context=".LegacyMainActivity">
 
     <com.google.android.material.button.MaterialButton
         android:id="@+id/launch_button"
diff --git a/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml b/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml
new file mode 100644
index 0000000..5b89e97
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values-night/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <string name="launch_string">Launch AOSP PDF Viewer</string>
+</resources>
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml b/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..df40dfd
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values-night/themes.xml
@@ -0,0 +1,23 @@
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Base.Theme.Androidx" parent="Theme.Material3.DayNight.NoActionBar">
+        <!-- Customize your dark theme here. -->
+        <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
+    </style>
+</resources>
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/res/values/colors.xml b/pdf/integration-tests/testapp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..8175d9c
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 787fa02..75de71e 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -30,6 +30,7 @@
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.exifinterface:exifinterface:1.3.2")
+    implementation(project(":core:core"))
 
     testImplementation(libs.junit)
     testImplementation(libs.testCore)
@@ -59,7 +60,9 @@
     namespace "androidx.pdf"
 
     defaultConfig {
-        minSdkVersion 30
+        minSdk 31
+        buildToolsVersion "35.0.0-rc1"
+        compileSdk 35
     }
 
     buildFeatures {
@@ -72,12 +75,6 @@
         }
     }
 
-    externalNativeBuild {
-        cmake {
-            path file('src/main/native/CMakeLists.txt')
-            version libs.versions.cmake.get()
-        }
-    }
     sourceSets {
         test {
             assets {
diff --git a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
index 1466502..fe945ea 100644
--- a/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
+++ b/pdf/pdf-viewer/src/androidTest/java/androidx/pdf/viewer/loader/PdfTaskExecutorTest.java
@@ -25,7 +25,7 @@
 import android.os.RemoteException;
 
 import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 
diff --git a/pdf/pdf-viewer/src/main/AndroidManifest.xml b/pdf/pdf-viewer/src/main/AndroidManifest.xml
index cd24525..eadf1dd 100644
--- a/pdf/pdf-viewer/src/main/AndroidManifest.xml
+++ b/pdf/pdf-viewer/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@
     <application android:label="PdfViewer">
 
         <service
-            android:name="androidx.pdf.pdflib.PdfDocumentService"
+            android:name="androidx.pdf.service.PdfDocumentService"
             android:isolatedProcess="true"
             tools:ignore="MissingServiceExportedEqualsTrue" />
 
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
index b029958..44ac3b1 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLink.java
@@ -18,14 +18,19 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 
 import com.google.common.base.Preconditions;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -77,6 +82,25 @@
     }
 
     /**
+     * Converts android.graphics.pdf.content.PdfPageGotoLinkContent object to its
+     * androidx.pdf.aidl.GotoLink representation.
+     */
+    @NonNull
+    public static GotoLink convert(@NonNull PdfPageGotoLinkContent pdfPageGotoLinkContent) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            List<Rect> rectBounds = new ArrayList<>();
+            List<RectF> rectFBounds = pdfPageGotoLinkContent.getBounds();
+            for (RectF rectF : rectFBounds) {
+                rectBounds.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+                        (int) rectF.bottom));
+            }
+            return new GotoLink(rectBounds,
+                    GotoLinkDestination.convert(pdfPageGotoLinkContent.getDestination()));
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    /**
      * Gets the bounds of a {@link GotoLink} represented as a list of {@link Rect}.
      * Links which are spread across multiple lines will be surrounded by multiple {@link Rect}
      * in order of viewing.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
index 11c2137..e7eae7f 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/GotoLinkDestination.java
@@ -17,8 +17,11 @@
 package androidx.pdf.models;
 
 import android.annotation.SuppressLint;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -140,4 +143,20 @@
         parcel.writeFloat(mYCoordinate);
         parcel.writeFloat(mZoom);
     }
+
+    /**
+     * Converts android.graphics.pdf.content.PdfPageGotoLinkContent.Destination object to its
+     * androidx.pdf.aidl.GotoLinkDestination representation.
+     */
+    @NonNull
+    public static GotoLinkDestination convert(
+            @NonNull PdfPageGotoLinkContent.Destination pdfPageGotoLinkContentDest) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            return new GotoLinkDestination(pdfPageGotoLinkContentDest.getPageNumber(),
+                    pdfPageGotoLinkContentDest.getXCoordinate(),
+                    pdfPageGotoLinkContentDest.getYCoordinate(),
+                    pdfPageGotoLinkContentDest.getZoom());
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
index 9d51435..640ef5c 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/LinkRects.java
@@ -18,8 +18,12 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -27,6 +31,7 @@
 import androidx.pdf.data.ListOfList;
 import androidx.pdf.util.Preconditions;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -114,4 +119,68 @@
         parcel.writeList(mLinkToRect);
         parcel.writeList(mUrls);
     }
+
+    /**
+     * Flattens the list of PdfPageLinkContent objects and converts to a LinkRects objects.
+     * <p>As an example, in case there are 2 weblinks on the page of the document with the 1st link
+     * overflowing to the next line, {@code List<PdfPageLinkContent>} would have the following
+     * values -
+     * <pre>
+     * List(
+     *      PdfPageLinkContent(
+     *          bounds = [RectF(l1, t1, r1, b1), RectF(l2, t2, r2, b2)],
+     *          url = url1
+     *      ),
+     *      PdfPageLinkContent(
+     *          bounds = [RectF(l3, t3, r3, b3)],
+     *          url = url2
+     *      ),
+     * )
+     *
+     * Using the method below, we can flatten the {@code List<PdfPageLinkContent>} to the following
+     * representation -
+     * LinkRects(
+     *      mRects=[Rect(l1, t1, r1, b1), Rect(l2, t2, r2, b2), Rect(l3, t3, r3, b3)],
+     *      mLinkToRect=[0,2],
+     *      mUrls=[url1, url2]
+     * )
+     * </pre>
+     */
+    @NonNull
+    public static LinkRects flattenList(@NonNull List<PdfPageLinkContent> pdfPageLinkContentList) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            List<Rect> rects = new ArrayList<>();
+            List<Integer> linkToRect = new ArrayList<>();
+            List<String> urls = new ArrayList<>();
+            int numRects = 0;
+            for (PdfPageLinkContent pdfPageLinkContent : pdfPageLinkContentList) {
+                List<RectF> rectFBounds = pdfPageLinkContent.getBounds();
+                for (RectF rectF : rectFBounds) {
+                    rects.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+                            (int) rectF.bottom));
+                }
+                urls.add(pdfPageLinkContent.getUri().toString());
+                linkToRect.add(numRects);
+                numRects += pdfPageLinkContent.getBounds().size();
+            }
+
+            return new LinkRects(rects, linkToRect, urls);
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    @NonNull
+    public List<Rect> getRects() {
+        return mRects;
+    }
+
+    @NonNull
+    public List<Integer> getLinkToRect() {
+        return mLinkToRect;
+    }
+
+    @NonNull
+    public List<String> getUrls() {
+        return mUrls;
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
index 4dade10..e12080e 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/MatchRects.java
@@ -18,8 +18,12 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -27,6 +31,7 @@
 import androidx.pdf.util.Preconditions;
 
 import java.util.AbstractList;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -161,4 +166,66 @@
         parcel.writeList(mMatchToRect);
         parcel.writeList(mCharIndexes);
     }
+
+    /**
+     * Flattens the list of PageMatchBounds objects and converts it to a MatchRects objects.
+     * <p>As an example, in case there are 2 matches on the page of the document with the 1st match
+     * overflowing to the next line, {@code List<PageMatchBounds>} would have the following values -
+     * <pre>
+     * List(
+     *      PageMatchBounds(
+     *          bounds = [RectF(l1, t1, r1, b1), RectF(l2, t2, r2, b2)],
+     *          mTextStartIndex = 1
+     *      ),
+     *      PageMatchBounds(
+     *          bounds = [RectF(l3, t3, r3, b3)],
+     *          mTextStartIndex = 3
+     *      ),
+     * )
+     *
+     * Using the method below, we can flatten the {@code List<PageMatchBounds>} to the following
+     * representation -
+     * MatchRects(
+     *      mRects=[Rect(l1, t1, r1, b1), Rect(l2, t2, r2, b2), Rect(l3, t3, r3, b3)],
+     *      mMatchToRect=[0,2],
+     *      mCharIndexes=[1, 3]
+     * )
+     * </pre>
+     */
+    @NonNull
+    public static MatchRects flattenList(@NonNull List<PageMatchBounds> pageMatchBoundsList) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            List<Rect> rects = new ArrayList<>();
+            List<Integer> matchToRect = new ArrayList<>();
+            List<Integer> charIndexes = new ArrayList<>();
+            int numRects = 0;
+            for (PageMatchBounds pageMatchBound : pageMatchBoundsList) {
+                List<RectF> rectFBounds = pageMatchBound.getBounds();
+                for (RectF rectF : rectFBounds) {
+                    rects.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+                            (int) rectF.bottom));
+                }
+                matchToRect.add(numRects);
+                numRects += pageMatchBound.getBounds().size();
+                charIndexes.add(pageMatchBound.getTextStartIndex());
+            }
+            return new MatchRects(rects, matchToRect, charIndexes);
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    @NonNull
+    public List<Rect> getRects() {
+        return mRects;
+    }
+
+    @NonNull
+    public List<Integer> getMatchToRect() {
+        return mMatchToRect;
+    }
+
+    @NonNull
+    public List<Integer> getCharIndexes() {
+        return mCharIndexes;
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
index 0ba0168..0e9d985 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/PageSelection.java
@@ -17,12 +17,17 @@
 package androidx.pdf.models;
 
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.os.Build;
 import android.os.Parcel;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.pdf.data.TextSelection;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /** Represents text selection on a particular page of a PDF. Immutable. */
@@ -82,13 +87,6 @@
     }
 
     @Override
-    public String toString() {
-        return String.format("PageSelection(page=%d, start=%s, stop=%s, %d rects)", mPage,
-                getStart(),
-                getStop(), mRects.size());
-    }
-
-    @Override
     public void writeToParcel(@NonNull Parcel parcel, int flags) {
         parcel.writeInt(mPage);
         parcel.writeParcelable(getStart(), 0);
@@ -96,4 +94,35 @@
         parcel.writeList(mRects);
         parcel.writeString(mText);
     }
+
+    /**
+     * Converts android.graphics.pdf.models.selection.PageSelection object to its
+     * androidx.pdf.aidl.PageSelection representation.
+     */
+    @NonNull
+    public static PageSelection convert(
+            @NonNull android.graphics.pdf.models.selection.PageSelection pageSelection) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            List<PdfPageTextContent> textSelections = pageSelection.getSelectedTextContents();
+
+            // TODO: Add list handling instead of taking its first element
+            String selectedText = textSelections.get(0).getText();
+
+            List<Rect> rectBounds = new ArrayList<Rect>();
+            // TODO: Add list handling instead of taking its first element
+            List<RectF> rectFBounds = textSelections.get(0).getBounds();
+            for (RectF rectF : rectFBounds) {
+                rectBounds.add(new Rect((int) rectF.left, (int) rectF.top, (int) rectF.right,
+                        (int) rectF.bottom));
+            }
+
+            return new PageSelection(pageSelection.getPage(),
+                    SelectionBoundary.convert(pageSelection.getStart(),
+                            pageSelection.getStart().getIsRtl()),
+                    SelectionBoundary.convert(pageSelection.getStop(),
+                            pageSelection.getStop().getIsRtl()),
+                    rectBounds, selectedText);
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
index 01294ab..e8c5b98 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/models/SelectionBoundary.java
@@ -18,8 +18,10 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Point;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -101,12 +103,6 @@
     }
 
     @Override
-    public String toString() {
-        String indexStr = (mIndex == Integer.MAX_VALUE) ? "MAX" : Integer.toString(mIndex);
-        return String.format("@%s (%d,%d)", indexStr, mX, mY);
-    }
-
-    @Override
     public void writeToParcel(@NonNull Parcel parcel, int flags) {
         parcel.writeIntArray(new int[]{mIndex, mX, mY, mIsRtl ? 1 : 0});
     }
@@ -135,4 +131,41 @@
         result = 31 * result + (mIsRtl ? 1231 : 1237);
         return result;
     }
+
+    /**
+     * Converts android.graphics.pdf.models.selection.SelectionBoundary object to its
+     * androidx.pdf.aidl.SelectionBoundary representation.
+     */
+    @NonNull
+    public static SelectionBoundary convert(
+            @NonNull android.graphics.pdf.models.selection.SelectionBoundary selectionBoundary,
+            boolean isRtl) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            if (selectionBoundary.getPoint() == null) {
+                return new SelectionBoundary(selectionBoundary.getIndex(), -1, -1, isRtl);
+            }
+            return new SelectionBoundary(selectionBoundary.getIndex(),
+                    selectionBoundary.getPoint().x,
+                    selectionBoundary.getPoint().y, isRtl);
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    /**
+     * Converts androidx.pdf.aidl.SelectionBoundary object to its
+     * android.graphics.pdf.models.selection.SelectionBoundary representation.
+     */
+    @NonNull
+    public static android.graphics.pdf.models.selection.SelectionBoundary convert(
+            @NonNull SelectionBoundary selectionBoundary) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            if (selectionBoundary.getIndex() == -1) {
+                return new android.graphics.pdf.models.selection.SelectionBoundary(
+                        new Point(selectionBoundary.getX(), selectionBoundary.getY()));
+            }
+            return new android.graphics.pdf.models.selection.SelectionBoundary(
+                    selectionBoundary.getIndex());
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java
deleted file mode 100644
index e7f6c28..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentService.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.pdf.pdflib;
-
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.os.IBinder;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.pdf.data.FutureValues;
-import androidx.pdf.models.Dimensions;
-import androidx.pdf.models.GotoLink;
-import androidx.pdf.models.LinkRects;
-import androidx.pdf.models.MatchRects;
-import androidx.pdf.models.PageSelection;
-import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.models.SelectionBoundary;
-
-import java.util.List;
-
-/** Isolated Service wrapper around the PdfClient native lib, for security purposes. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentService extends Service {
-
-    private static final String TAG = "PdfDocumentService";
-
-    @NonNull
-    @Override
-    public IBinder onBind(Intent intent) {
-        return new PdfDocumentRemoteImpl();
-    }
-
-    @Override
-    public boolean onUnbind(Intent intent) {
-        return super.onUnbind(intent);
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-    }
-
-    private static class PdfDocumentRemoteImpl extends PdfDocumentRemote.Stub {
-
-        private final FutureValues.BlockingCallback<Boolean> mLoaderCallback =
-                new FutureValues.BlockingCallback<>();
-
-        private PdfDocument mPdfDocument;
-
-        PdfDocumentRemoteImpl() {
-        }
-
-        @Override
-        public int create(ParcelFileDescriptor pfd, String password) throws RemoteException {
-            mLoaderCallback.getBlocking();
-            ensurePdfDestroyed();
-            int fd = pfd.detachFd();
-            LoadPdfResult result = PdfDocument.createFromFd(fd, password);
-            if (result.isLoaded()) {
-                mPdfDocument = result.getPdfDocument();
-            }
-            return result.getStatus().getNumber();
-        }
-
-        @Override
-        public int numPages() {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.numPages();
-        }
-
-        @Override
-        public Dimensions getPageDimensions(int pageNum) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getPageDimensions(pageNum);
-        }
-
-        @Override
-        public String getPageText(int pageNum) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getPageText(pageNum);
-        }
-
-        @Override
-        public List<String> getPageAltText(int pageNum) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getPageAltText(pageNum);
-        }
-
-        @Override
-        public Bitmap renderPage(int pageNum, int pageWidth, int pageHeight,
-                boolean hideTextAnnots) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.renderPageFd(pageNum, pageWidth, pageHeight, hideTextAnnots);
-        }
-
-        @Override
-        public Bitmap renderTile(int pageNum, int tileWidth, int tileHeight, int scaledPageWidth,
-                int scaledPageHeight, int left, int top, boolean hideTextAnnots) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.renderTileFd(pageNum, tileWidth, tileHeight, scaledPageWidth,
-                    scaledPageHeight, left, top, hideTextAnnots);
-        }
-
-        @Override
-        public MatchRects searchPageText(int pageNum, String query) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.searchPageText(pageNum, query);
-        }
-
-        @Override
-        public PageSelection selectPageText(int pageNum, SelectionBoundary start,
-                SelectionBoundary stop) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.selectPageText(pageNum, start, stop);
-        }
-
-        @Override
-        public LinkRects getPageLinks(int pageNum) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getPageLinks(pageNum);
-        }
-
-        @Override
-        public List<GotoLink> getPageGotoLinks(int pageNum) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getPageGotoLinks(pageNum);
-        }
-
-        @Override
-        public boolean isPdfLinearized() {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.isPdfLinearized();
-        }
-
-        @Override
-        public int getFormType() {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.getFormType();
-        }
-
-        @Override
-        protected void finalize() throws Throwable {
-            mLoaderCallback.getBlocking();
-            ensurePdfDestroyed();
-            super.finalize();
-        }
-
-        private void ensurePdfDestroyed() {
-            if (mPdfDocument != null) {
-                try {
-                    mPdfDocument.destroy();
-                } catch (Throwable ignored) {
-                }
-            }
-            mPdfDocument = null;
-        }
-
-        @Override
-        public boolean cloneWithoutSecurity(ParcelFileDescriptor destination) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.cloneWithoutSecurity(destination);
-        }
-
-        @Override
-        public boolean saveAs(ParcelFileDescriptor destination) {
-            mLoaderCallback.getBlocking();
-            return mPdfDocument.saveAs(destination);
-        }
-    }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
similarity index 98%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
index 5ce75a3..fcd7c7a 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/LoadPdfResult.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/LoadPdfResult.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
similarity index 98%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
index 66fef2ba..0818d63 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocument.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocument.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
 
 import android.graphics.Bitmap;
 import android.os.ParcelFileDescriptor;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
similarity index 97%
rename from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
rename to pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
index 5138d2d..d9e62a5 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentRemoteProto.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.pdf.pdflib;
+package androidx.pdf.service;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java
new file mode 100644
index 0000000..c6e1956
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfDocumentService.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.pdf.PdfRendererPreV;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.graphics.pdf.content.PdfPageImageContent;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.PdfStatus;
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.GotoLink;
+import androidx.pdf.models.LinkRects;
+import androidx.pdf.models.MatchRects;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.models.PdfDocumentRemote;
+import androidx.pdf.models.SelectionBoundary;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Isolated Service wrapper around the PdfClient native lib, for security purposes. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PdfDocumentService extends Service {
+
+    @NonNull
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new PdfDocumentRemoteImpl();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+    private static class PdfDocumentRemoteImpl extends PdfDocumentRemote.Stub {
+        private PdfRendererAdapter mAdapter;
+
+        PdfDocumentRemoteImpl() {
+        }
+
+        @Override
+        public int create(ParcelFileDescriptor pfd, String password) {
+            try {
+                mAdapter = new PdfRendererAdapter(pfd, password);
+                return PdfStatus.LOADED.getNumber();
+            } catch (SecurityException e) {
+                return PdfStatus.REQUIRES_PASSWORD.getNumber();
+            } catch (Exception e) {
+                return PdfStatus.PDF_ERROR.getNumber();
+            }
+        }
+
+        @Override
+        public int numPages() {
+            return mAdapter.getPageCount();
+        }
+
+        @Override
+        public Dimensions getPageDimensions(int pageNum) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                return new Dimensions(pageAdapter.getWidth(),
+                        pageAdapter.getHeight());
+            }
+        }
+
+        @Override
+        public Bitmap renderPage(int pageNum, int pageWidth, int pageHeight,
+                boolean hideTextAnnots) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                Bitmap output = Bitmap.createBitmap(pageWidth, pageHeight, Bitmap.Config.ARGB_8888);
+                output.eraseColor(Color.WHITE);
+                pageAdapter.render(output);
+                return output;
+            }
+        }
+
+        @Override
+        public Bitmap renderTile(int pageNum, int tileWidth, int tileHeight, int scaledPageWidth,
+                int scaledPageHeight, int left, int top, boolean hideTextAnnots) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                Bitmap output = Bitmap.createBitmap(tileWidth, tileHeight, Bitmap.Config.ARGB_8888);
+                output.eraseColor(Color.WHITE);
+                pageAdapter.renderTile(output, left, top, scaledPageWidth, scaledPageHeight);
+                return output;
+            }
+        }
+
+
+        @Override
+        public String getPageText(int pageNum) {
+            if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+                try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                    List<PdfPageTextContent> textPdfContentList = pageAdapter.getPageTextContents();
+                    // TODO: Add list handling instead of taking its first element
+                    return textPdfContentList.get(0).getText();
+                }
+            }
+            throw new UnsupportedOperationException("Operation support above S");
+        }
+
+        @Override
+        public List<String> getPageAltText(int pageNum) {
+            if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+                try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                    List<PdfPageImageContent> text = pageAdapter.getPageImageContents();
+                    return text.stream().map(PdfPageImageContent::getAltText).collect(
+                            Collectors.toList());
+                }
+            }
+            throw new UnsupportedOperationException("Operation support above S");
+        }
+
+        @Override
+        public MatchRects searchPageText(int pageNum, String query) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                List<PageMatchBounds> searchResultList = pageAdapter.searchPageText(query);
+                return MatchRects.flattenList(searchResultList);
+            }
+        }
+
+        @Override
+        public PageSelection selectPageText(int pageNum, SelectionBoundary start,
+                SelectionBoundary stop) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                android.graphics.pdf.models.selection.PageSelection pageSelection =
+                        pageAdapter.selectPageText(SelectionBoundary.convert(start),
+                                SelectionBoundary.convert(stop));
+                if (pageSelection != null) {
+                    return PageSelection.convert(pageSelection);
+                }
+                return null;
+            }
+        }
+
+        @Override
+        public LinkRects getPageLinks(int pageNum) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                List<PdfPageLinkContent> pageLinks = pageAdapter.getPageLinks();
+                return LinkRects.flattenList(pageLinks);
+            }
+        }
+
+        @Override
+        public List<GotoLink> getPageGotoLinks(int pageNum) {
+            try (PdfPageAdapter pageAdapter = mAdapter.openPage(pageNum)) {
+                List<PdfPageGotoLinkContent> gotoLinks = pageAdapter.getPageGotoLinks();
+                if (!gotoLinks.isEmpty()) {
+                    List<GotoLink> list = new ArrayList<>();
+                    for (PdfPageGotoLinkContent link : gotoLinks) {
+                        GotoLink convertedLink = GotoLink.convert(link);
+                        list.add(convertedLink);
+                    }
+                    return list;
+                }
+                return null;
+            }
+        }
+
+        @Override
+        public boolean isPdfLinearized() {
+            return mAdapter.getDocumentLinearizationType()
+                    == PdfRendererPreV.DOCUMENT_LINEARIZED_TYPE_LINEARIZED;
+        }
+
+        @Override
+        public int getFormType() {
+            return mAdapter.getDocumentFormType();
+
+        }
+
+        @Override
+        public boolean cloneWithoutSecurity(ParcelFileDescriptor destination) {
+            // TODO: Implementation pending as use-case undiscovered.
+            return true;
+        }
+
+        @Override
+        public boolean saveAs(ParcelFileDescriptor destination) {
+            // TODO: Implementation pending as use-case undiscovered.
+            return true;
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            mAdapter.close();
+            mAdapter = null;
+            super.finalize();
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java
new file mode 100644
index 0000000..4d17dfe
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfPageAdapter.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.service;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.pdf.PdfRenderer;
+import android.graphics.pdf.PdfRendererPreV;
+import android.graphics.pdf.RenderParams;
+import android.graphics.pdf.content.PdfPageGotoLinkContent;
+import android.graphics.pdf.content.PdfPageImageContent;
+import android.graphics.pdf.content.PdfPageLinkContent;
+import android.graphics.pdf.content.PdfPageTextContent;
+import android.graphics.pdf.models.PageMatchBounds;
+import android.graphics.pdf.models.selection.PageSelection;
+import android.graphics.pdf.models.selection.SelectionBoundary;
+import android.os.Build;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Supplier;
+
+import java.util.List;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PdfPageAdapter implements AutoCloseable {
+    private int mPageNum;
+    private int mHeight;
+    private int mWidth;
+
+    private PdfRenderer.Page mPdfRendererPage;
+    private PdfRendererPreV.Page mPdfRendererPreVPage;
+
+    PdfPageAdapter(@NonNull PdfRenderer pdfRenderer, int pageNum) {
+        mPageNum = pageNum;
+        mPdfRendererPage = pdfRenderer.openPage(pageNum);
+        mHeight = mPdfRendererPage.getHeight();
+        mWidth = mPdfRendererPage.getWidth();
+    }
+
+    PdfPageAdapter(@NonNull PdfRendererPreV pdfRendererPreV, int pageNum) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            mPageNum = pageNum;
+            mPdfRendererPreVPage = pdfRendererPreV.openPage(pageNum);
+            mHeight = mPdfRendererPreVPage.getHeight();
+            mWidth = mPdfRendererPreVPage.getWidth();
+        }
+    }
+
+    public int getPageNum() {
+        return mPageNum;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public void render(@NonNull Bitmap bitmap) {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            mPdfRendererPage.render(bitmap, null, null, getRenderParams());
+        } else {
+            checkAndExecute(
+                    () -> mPdfRendererPreVPage.render(bitmap, null, null, getRenderParams()));
+        }
+    }
+
+    public void renderTile(@NonNull Bitmap bitmap,
+            int left, int top, int scaledPageWidth, int scaledPageHeight) {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            int pageWidth = mPdfRendererPage.getWidth();
+            int pageHeight = mPdfRendererPage.getHeight();
+            Matrix transform = getTransformationMatrix(left, top, (float) scaledPageWidth,
+                    (float) scaledPageHeight, pageWidth,
+                    pageHeight);
+            RenderParams renderParams = getRenderParams();
+            mPdfRendererPage.render(bitmap, null, transform, renderParams);
+        } else {
+            checkAndExecute(() -> {
+                {
+                    int pageWidth = mPdfRendererPreVPage.getWidth();
+                    int pageHeight = mPdfRendererPreVPage.getHeight();
+                    Matrix transform = getTransformationMatrix(left, top, (float) scaledPageWidth,
+                            (float) scaledPageHeight, pageWidth,
+                            pageHeight);
+                    RenderParams renderParams = getRenderParams();
+                    mPdfRendererPreVPage.render(bitmap, null, transform, renderParams);
+                }
+            });
+        }
+    }
+
+    @NonNull
+    public List<PdfPageTextContent> getPageTextContents() {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.getTextContents();
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.getTextContents());
+    }
+
+    @NonNull
+    public List<PdfPageImageContent> getPageImageContents() {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.getImageContents();
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.getImageContents());
+    }
+
+    @Nullable
+    public PageSelection selectPageText(@NonNull SelectionBoundary start,
+            @NonNull SelectionBoundary stop) {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.selectContent(start, stop);
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.selectContent(start, stop));
+    }
+
+    @NonNull
+    public List<PageMatchBounds> searchPageText(@NonNull String query) {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.searchText(query);
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.searchText(query));
+    }
+
+    @NonNull
+    public List<PdfPageLinkContent> getPageLinks() {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.getLinkContents();
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.getLinkContents());
+    }
+
+    @NonNull
+    public List<PdfPageGotoLinkContent> getPageGotoLinks() {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            return mPdfRendererPage.getGotoLinks();
+        }
+        return checkAndExecute(() -> mPdfRendererPreVPage.getGotoLinks());
+
+    }
+
+    private Matrix getTransformationMatrix(int left, int top, float scaledPageWidth,
+            float scaledPageHeight,
+            int pageWidth, int pageHeight) {
+        Matrix matrix = new Matrix();
+        matrix.setScale(scaledPageWidth / pageWidth,
+                scaledPageHeight / pageHeight);
+        matrix.postTranslate(-left, -top);
+        return matrix;
+    }
+
+    private RenderParams getRenderParams() {
+        return checkAndExecute(() -> {
+            RenderParams.Builder renderParamsBuilder = new RenderParams.Builder(
+                    RenderParams.RENDER_MODE_FOR_DISPLAY);
+            return renderParamsBuilder.setRenderFlags(0).build();
+        });
+    }
+
+    private static void checkAndExecute(@NonNull Runnable block) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            block.run();
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    private static <T> T checkAndExecute(@NonNull Supplier<T> block) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            return block.get();
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    @Override
+    public void close() {
+        if (mPdfRendererPage != null && BuildCompat.isAtLeastV()) {
+            mPdfRendererPage.close();
+            mPdfRendererPage = null;
+        } else {
+            checkAndExecute(() -> {
+                mPdfRendererPreVPage.close();
+                mPdfRendererPreVPage = null;
+            });
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java
new file mode 100644
index 0000000..7746046
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/service/PdfRendererAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.service;
+
+import android.graphics.pdf.LoadParams;
+import android.graphics.pdf.PdfRenderer;
+import android.graphics.pdf.PdfRendererPreV;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.ext.SdkExtensions;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Supplier;
+
+import java.io.IOException;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PdfRendererAdapter implements AutoCloseable {
+    private PdfRenderer mPdfRenderer;
+    private PdfRendererPreV mPdfRendererPreV;
+
+    PdfRendererAdapter(@NonNull ParcelFileDescriptor parcelFileDescriptor,
+            @NonNull String password)
+            throws IOException, SecurityException {
+        if (BuildCompat.isAtLeastV()) {
+            LoadParams params = new LoadParams.Builder().setPassword(password).build();
+            mPdfRenderer = new PdfRenderer(parcelFileDescriptor, params);
+        } else {
+            if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+                LoadParams params = new LoadParams.Builder().setPassword(password).build();
+                mPdfRendererPreV = new PdfRendererPreV(parcelFileDescriptor, params);
+            }
+        }
+    }
+
+    /**  */
+    @NonNull
+    PdfPageAdapter openPage(int pageNum) {
+        if (mPdfRenderer != null) {
+            return new PdfPageAdapter(mPdfRenderer, pageNum);
+        }
+        return new PdfPageAdapter(mPdfRendererPreV, pageNum);
+    }
+
+    public int getPageCount() {
+        if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+            return mPdfRenderer.getPageCount();
+        }
+        return checkAndExecute(() -> mPdfRendererPreV.getPageCount());
+    }
+
+    public int getDocumentLinearizationType() {
+        if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+            return mPdfRenderer.getDocumentLinearizationType();
+        }
+        return checkAndExecute(() -> mPdfRendererPreV.getDocumentLinearizationType());
+    }
+
+    public int getDocumentFormType() {
+        if (mPdfRenderer != null && BuildCompat.isAtLeastV()) {
+            return mPdfRenderer.getPdfFormType();
+        }
+        return checkAndExecute(() -> mPdfRendererPreV.getPdfFormType());
+    }
+
+    private static void checkAndExecute(@NonNull Runnable block) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            block.run();
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    private static <T> T checkAndExecute(@NonNull Supplier<T> block) {
+        if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
+            return block.get();
+        }
+        throw new UnsupportedOperationException("Operation support above S");
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mPdfRenderer != null) {
+            mPdfRenderer.close();
+            mPdfRenderer = null;
+        } else {
+            checkAndExecute(() -> {
+                mPdfRendererPreV.close();
+                mPdfRendererPreV = null;
+            });
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
index a33af08..0133a59 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AnnotationUtils.java
@@ -39,8 +39,8 @@
 
     private AnnotationUtils() {}
 
-    /** Launches the annotation intent for a given Uri */
-    public static boolean launchAnnotationIntent(@NonNull Context context, @NonNull Uri uri) {
+    /** Returns true if there is an activity that can resolve the annotation intent else false. */
+    public static boolean resolveAnnotationIntent(@NonNull Context context, @NonNull Uri uri) {
         Objects.requireNonNull(context);
         Objects.requireNonNull(uri);
         Intent intent = getAnnotationIntent(uri);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java
deleted file mode 100644
index aa6979e..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/BitmapParcel.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.pdf.util;
-
-import android.graphics.Bitmap;
-import android.os.ParcelFileDescriptor;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-
-import java.io.IOException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Utility to share {@link Bitmap}s across processes using a {@link android.os.Parcelable} reference
- * that can fit safely in an Intent.
- *
- * <p>A {@link BitmapParcel} wraps a {@link Bitmap} instance and exposes an output file descriptor
- * that can be used to fill in the bytes of the wrapped bitmap from any process.
- *
- * <p>Uses a piped file descriptor, and natively reads and copies bytes from the source end into the
- * {@link Bitmap}'s byte buffer. Runs on a new Thread.
- *
- * <p>Note: Only one-way transfers are implemented (write into a bitmap from any source).
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class BitmapParcel {
-
-    private static final String TAG = BitmapParcel.class.getSimpleName();
-    private final Bitmap mBitmap;
-    private final Timer mTimer = Timer.start();
-    private CountDownLatch mCountDownLatch;
-
-    /**
-     * Creates a BitmapParcel that allows writing bytes into the given {@link Bitmap}.
-     *
-     * @param bitmap the destination bitmap: its contents will be replaced by what is sent on the
-     *               fd.
-     */
-    public BitmapParcel(@NonNull Bitmap bitmap) {
-        this.mBitmap = bitmap;
-    }
-
-    /** Opens a file descriptor that will write into the wrapped bitmap. */
-    @Nullable
-    public ParcelFileDescriptor openOutputFd() {
-        ParcelFileDescriptor[] pipe;
-        try {
-            // TODO: StrictMode - close() is not explicitly called.
-            pipe = ParcelFileDescriptor.createPipe();
-        } catch (IOException e) {
-            return null;
-        }
-        ParcelFileDescriptor source = pipe[0];
-        ParcelFileDescriptor sink = pipe[1];
-        receiveAsync(source);
-        return sink;
-    }
-
-    /** Terminates any running copy and close all resources. */
-    public void close() {
-        if (mCountDownLatch != null) {
-            boolean timedOut = false;
-            try {
-                timedOut = !mCountDownLatch.await(5, TimeUnit.SECONDS);
-            } catch (InterruptedException ignored) {
-            }
-        }
-    }
-
-    /** Receives the bitmap's bytes from a file descriptor. Runs on a new thread. */
-    private void receiveAsync(final ParcelFileDescriptor source) {
-        mCountDownLatch = new CountDownLatch(1);
-        new Thread(
-                () -> {
-                    Timer t = Timer.start();
-                    receiveBitmap(mBitmap, source);
-                    mCountDownLatch.countDown();
-                },
-                "Pico-AsyncPipedFdNative.receiveAsync")
-                .start();
-    }
-
-    /**
-     * Reads bytes from a file descriptor into a {@link Bitmap}, using a native memcpy.
-     *
-     * @param bitmap   A bitmap whose pixels to populate.
-     * @param sourceFd The source file descriptor.
-     */
-    protected void receiveBitmap(@NonNull Bitmap bitmap, @NonNull ParcelFileDescriptor sourceFd) {
-        readIntoBitmap(bitmap, sourceFd.detachFd());
-    }
-
-    /**
-     * Reads bytes from the given file descriptor and fill in the Bitmap instance.
-     *
-     * @param bitmap   A bitmap whose pixels to populate.
-     * @param sourceFd The source file descriptor. Will be closed after transfer.
-     */
-    private static native boolean readIntoBitmap(Bitmap bitmap, int sourceFd);
-
-    /**
-     *
-     */
-    public static void loadNdkLib() {
-        System.loadLibrary("bitmapParcel");
-    }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java
new file mode 100644
index 0000000..9cf2139
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/PaginationUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.util;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Utils class for [PaginatedView]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PaginationUtils {
+    private PaginationUtils() {
+    }
+
+    /** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
+    private static final int PAGE_ELEVATION_DP = 2;
+
+    /** Converts a value given in dp to pixels, based on the screen density. */
+    public static int getPageElevationInPixels(@NonNull Context context) {
+        float density = context.getResources().getDisplayMetrics().density;
+        return (int) (PAGE_ELEVATION_DP * density);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
index 6711671..fd9ce63 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ZoomUtils.java
@@ -38,4 +38,91 @@
             return outerHeight / innerHeight;
         }
     }
+
+    /**
+     * Used to convert the zoom view coordinates to the content coordinates using the current
+     * zoom and scroll values.
+     *
+     * @param zoomViewCoordinate coordinate for either the x or y-axis in the [ZoomView] viewport.
+     * @param zoom               current zoom factor.
+     * @param scroll             current scroll position.
+     * @return content coordinates converted from the viewport coordinates.
+     */
+    public static float toContentCoordinate(float zoomViewCoordinate, float zoom, int scroll) {
+        Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive.");
+        return (zoomViewCoordinate + scroll) / zoom;
+    }
+
+    /**
+     * Used to convert the content coordinates to the view coordinates using the current
+     * zoom and scroll values.
+     *
+     * @param contentCoordinate coordinate for either the x or y-axis in the [ZoomView] viewport.
+     * @param zoom              current zoom factor.
+     * @param scroll            current scroll position.
+     * @return view coordinates converted from the content coordinates.
+     */
+    public static float toZoomViewCoordinate(float contentCoordinate, float zoom, int scroll) {
+        Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive.");
+        return (contentCoordinate * zoom) - scroll;
+    }
+
+    /**
+     * Used to find the delta between the view port pivot and the pivot after the zoom in/out is
+     * done. Delta is positive in case of zooming in, negative in case it has been zoomed out and
+     * 0 if no change.
+     *
+     * @param oldZoom   previous zoom factor.
+     * @param newZoom   current zoom factor.
+     * @param zoomPivot pivot point from zoom.
+     * @param scroll    scroll position.
+     * @return delta between the view port and zoomed in/out pivot.
+     */
+    public static int scrollDeltaNeededForZoomChange(
+            float oldZoom, float newZoom, float zoomPivot, int scroll) {
+        // Find where the given pivot point would move to when we change the zoom, and return the
+        // delta.
+        float contentPivot = ZoomUtils.toContentCoordinate(zoomPivot, oldZoom, scroll);
+        float movedZoomViewPivot = ZoomUtils.toZoomViewCoordinate(contentPivot, newZoom,
+                scroll);
+        return (int) (movedZoomViewPivot - zoomPivot);
+    }
+
+    /**
+     * Used to constrain the coordinate using the current zoom factor and the scroll position
+     * based on the content raw size and the view port size. In case of adjusting the x
+     * coordinate, the content and view port dimension will be the width while in case of
+     * y-coordinate it will be the width. Lower and upper bound is the left and right of the
+     * content for x-axis and is top and bottom for y-axis.
+     *
+     * @param zoom                current zoom factor.
+     * @param scroll              current scroll position.
+     * @param contentRawDimension raw dimension (height/width) for the content
+     * @param viewportDimension   viewport dimension
+     * @return scaled coordinate or 0 if no adjustment is needed.
+     */
+    public static int constrainCoordinate(float zoom, int scroll, int contentRawDimension,
+            int viewportDimension) {
+        float lowerBound = ZoomUtils.toZoomViewCoordinate(0, zoom, scroll);
+        float upperBound = ZoomUtils.toZoomViewCoordinate(contentRawDimension, zoom, scroll);
+
+        if (lowerBound <= 0 && upperBound >= viewportDimension) {
+            // Content too large for viewport and no dead margins: no adjustment needed.
+            return 0;
+        }
+
+        float scaledContentSize = upperBound - lowerBound;
+        if (scaledContentSize <= viewportDimension) {
+            // Content fits in viewport: keep in the center.
+            return (int) ((upperBound + lowerBound - viewportDimension) / 2);
+        } else {
+            // Content doesn't fit in viewport: eliminate dead margins.
+            if (lowerBound > 0) { // Dead margin on the left.
+                return (int) lowerBound;
+            } else if (upperBound < viewportDimension) { // Dead margin on the right.
+                return (int) (upperBound - viewportDimension);
+            }
+        }
+        return 0;
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
index 11eb417..6575393 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java
@@ -52,7 +52,7 @@
         super(context, attrs);
     }
 
-    public AbstractPaginatedView(@NonNull Context context, @NonNull AttributeSet attrs,
+    public AbstractPaginatedView(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr) {
         super(context, attrs, defStyleAttr);
     }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java
new file mode 100644
index 0000000..ae535e2
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/FastScrollPositionValueObserver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import androidx.annotation.Nullable;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.widget.FastScrollView;
+
+class FastScrollPositionValueObserver implements ObservableValue.ValueObserver<Integer> {
+    private final FastScrollView mFastScrollView;
+    private final PageIndicator mPageIndicator;
+
+    FastScrollPositionValueObserver(FastScrollView fastScrollView, PageIndicator pageIndicator) {
+        mFastScrollView = fastScrollView;
+        mPageIndicator = pageIndicator;
+    }
+
+    @Override
+    public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
+        if (mPageIndicator != null && newValue != null) {
+            mPageIndicator.getView().setY(
+                    newValue - (mPageIndicator.getView().getHeight() / 2));
+            mPageIndicator.show();
+            showFastScrollView();
+        }
+    }
+
+    private void showFastScrollView() {
+        if (mFastScrollView != null) {
+            mFastScrollView.setVisible();
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java
new file mode 100644
index 0000000..5491475
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/LayoutHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ThreadUtils;
+import androidx.pdf.viewer.loader.PdfLoader;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class LayoutHandler {
+    /** Only interact with Queue on the main thread. */
+    private final List<OnDimensCallback> mDimensCallbackQueue;
+    private final PdfLoader mPdfLoader;
+
+    /** The number of pages that have been laid out in the document. */
+    private int mPageLayoutReach = 0;
+    private int mInitialPageLayoutReach = 4;
+
+    public LayoutHandler(@NonNull PdfLoader pdfLoader) {
+        mDimensCallbackQueue = new ArrayList<>();
+        mPdfLoader = pdfLoader;
+    }
+
+    public int getPageLayoutReach() {
+        return mPageLayoutReach;
+    }
+
+    public void setPageLayoutReach(int pageLayoutReach) {
+        mPageLayoutReach = pageLayoutReach;
+    }
+
+    public int getInitialPageLayoutReach() {
+        return mInitialPageLayoutReach;
+    }
+
+    public void setInitialPageLayoutReach(int initialPageLayoutReach) {
+        mInitialPageLayoutReach = initialPageLayoutReach;
+    }
+
+    public void setInitialPageLayoutReachWithMax(int layoutReach) {
+        mInitialPageLayoutReach = Math.max(mInitialPageLayoutReach, layoutReach);
+    }
+
+    /**
+     * Lay out some pages up to some distant page. Not guaranteed to lay out any pages: maybe all
+     * pages, or at least enough pages, are already laid out.
+     */
+    public void maybeLayoutPages(int current) {
+        int peekAhead = Math.min(current + 2, 100);
+        int distantPage = Math.max(current + peekAhead, mInitialPageLayoutReach);
+        layoutPages(distantPage);
+    }
+
+    /**
+     * Lays out all the pages until {@code untilPage}, or equivalently so that {@code untilPage}s
+     * are laid out. So calling with {@code untilPage = 10} will ensure pages 0-9 are laid out.
+     *
+     * @param untilPage The upper limit of the range of pages to be laid out. Cropped to the
+     *                  number of pages of the document if this number was larger.
+     */
+    public void layoutPages(int untilPage) {
+        if (mPdfLoader == null) {
+            return;
+        }
+        int lastPage = Math.min(untilPage, mPdfLoader.getNumPages());
+        int requestLayoutPage = mPageLayoutReach;
+        while (requestLayoutPage < lastPage) {
+            mPdfLoader.loadPageDimensions(requestLayoutPage);
+            requestLayoutPage++;
+        }
+    }
+
+    /** */
+    public void add(@NonNull OnDimensCallback callback) {
+        mDimensCallbackQueue.add(callback);
+    }
+
+    /** */
+    public void processCallbacksInQueue(@NonNull Viewer.ViewState viewState, int pageNum) {
+        ThreadUtils.postOnUiThread(
+                () -> {
+                    if (mDimensCallbackQueue.isEmpty()
+                            || viewState == Viewer.ViewState.NO_VIEW) {
+                        return;
+                    }
+
+                    Iterator<OnDimensCallback> iterator =
+                            mDimensCallbackQueue.iterator();
+                    while (iterator.hasNext()) {
+                        OnDimensCallback callback = iterator.next();
+                        boolean shouldKeep = callback.onDimensLoaded(pageNum);
+                        if (!shouldKeep) {
+                            iterator.remove();
+                        }
+                    }
+                });
+    }
+
+    /** Callback is called everytime dimensions for a page have loaded. */
+    public interface OnDimensCallback {
+        /** Return true to continue receiving callbacks, else false. */
+        boolean onDimensLoaded(int pageNum);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
index b0f95d3..17a203d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageMosaicView.java
@@ -32,6 +32,7 @@
 import androidx.pdf.models.LinkRects;
 import androidx.pdf.util.Accessibility;
 import androidx.pdf.util.BitmapRecycler;
+import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.pdf.widget.MosaicView;
 
 import java.util.List;
@@ -40,7 +41,6 @@
  * Renders one Page.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressWarnings("UnusedVariable")
 public class PageMosaicView extends MosaicView implements PageViewFactory.PageView {
 
     @VisibleForTesting
@@ -50,19 +50,28 @@
     private String mPageText;
     private LinkRects mUrlLinks;
     private List<GotoLink> mGotoLinks;
+    private final PdfLoader mPdfLoader;
+    private final PdfSelectionModel mSelectionModel;
+    private final SearchModel mSearchModel;
 
     public PageMosaicView(
             @NonNull Context context,
             int pageNum,
             @NonNull Dimensions pageSize,
             @NonNull BitmapSource bitmapSource,
-            @Nullable BitmapRecycler bitmapRecycler) {
+            @Nullable BitmapRecycler bitmapRecycler,
+            @NonNull PdfLoader pdfLoader,
+            @NonNull PdfSelectionModel selectionModel,
+            @NonNull SearchModel searchModel) {
         super(context);
         this.mPageNum = pageNum;
         init(pageSize, bitmapRecycler, bitmapSource);
         setId(pageNum);
         setPageText(null);
         setFocusableInTouchMode(true);
+        this.mPdfLoader = pdfLoader;
+        this.mSelectionModel = selectionModel;
+        this.mSearchModel = searchModel;
     }
 
     /** Set the given overlay. */
@@ -169,4 +178,38 @@
     public View asView() {
         return this;
     }
+
+    /**
+     * Loads the page content like page text, external urls and goto links and also resets the
+     * overlays from selection and search
+     */
+    public void refreshPageContentAndOverlays() {
+        loadPageComponents();
+        resetOverlays();
+    }
+
+    /** Loads the page text, external links and the goto links for the page */
+    private void loadPageComponents() {
+        if (needsPageText()) {
+            mPdfLoader.loadPageText(mPageNum);
+        }
+        if (!hasPageUrlLinks()) {
+            mPdfLoader.loadPageUrlLinks(mPageNum);
+        }
+        if (!hasPageGotoLinks()) {
+            mPdfLoader.loadPageGotoLinks(mPageNum);
+        }
+    }
+
+    private void resetOverlays() {
+        if (getPageNum() == mSelectionModel.getPage()) {
+            setOverlay(new PdfHighlightOverlay(mSelectionModel.selection().get()));
+        } else if (mSearchModel.query().get() != null) {
+            if (!hasOverlay()) {
+                mPdfLoader.searchPageText(getPageNum(), mSearchModel.query().get());
+            }
+        } else {
+            setOverlay(null);
+        }
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
new file mode 100644
index 0000000..8936f4e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageRangeHandler.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.util.Preconditions;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class PageRangeHandler {
+    private static final int PAGE_PREFETCH_RADIUS = 1;
+
+    private final PaginationModel mPaginationModel;
+
+    /** The range of currently visible pages. */
+    private Range mVisiblePages = null;
+
+    /** The highest number page reached. */
+    private int mMaxPage = -1;
+
+    PageRangeHandler(PaginationModel paginationModel) {
+        this.mPaginationModel = paginationModel;
+        this.mVisiblePages = new Range();
+    }
+
+    @Nullable
+    public Range getVisiblePages() {
+        return mVisiblePages;
+    }
+
+    public void setVisiblePages(@Nullable Range visiblePages) {
+        mVisiblePages = visiblePages;
+    }
+
+    public int getMaxPage() {
+        return mMaxPage;
+    }
+
+    public void setMaxPage(int maxPage) {
+        mMaxPage = maxPage;
+    }
+
+    @NonNull
+    public PaginationModel getPaginationModel() {
+        return mPaginationModel;
+    }
+
+    /**
+     * Returns the page currently roughly centered in the view.
+     */
+    public int getVisiblePage() {
+        return (mVisiblePages != null) ? (mVisiblePages.getFirst() + mVisiblePages.getLast()) / 2
+                : 0;
+    }
+
+    /**
+     * Updates the max page to the upper bound of the visible page range if the upper bound is
+     * greater than the current max page
+     */
+    public void adjustMaxPageToUpperVisibleRange() {
+        if (mVisiblePages != null) {
+            mMaxPage = Math.max(mVisiblePages.getLast(), mMaxPage);
+        }
+    }
+
+    /** Updates the visible page range based on the y-scroll, current zoom and the view height */
+    public void refreshVisiblePageRange(int scrollY, float zoom, int viewHeight) {
+        mVisiblePages = computeVisibleRange(scrollY, zoom, viewHeight, true);
+    }
+
+    /** Computes the range of visible pages in the given position. */
+    @NonNull
+    public Range computeVisibleRange(int scrollY, float zoom, int viewHeight,
+            boolean includePartial) {
+        Preconditions.checkArgument(zoom > 0, "Zoom factor must be positive!");
+
+        int top = Math.round(scrollY / zoom);
+        int bottom = Math.round((scrollY + viewHeight) / zoom);
+        Range window = new Range(top, bottom);
+        return mPaginationModel.getPagesInWindow(window, includePartial);
+    }
+
+    /** Returns the range of pages within the prefetch radius of the visible pages. */
+    @NonNull
+    public Range getNearPagesToVisibleRange() {
+        Range allPages = new Range(0, mPaginationModel.getSize() - 1);
+        return mVisiblePages.expand(PAGE_PREFETCH_RADIUS, allPages);
+    }
+
+    /** Returns the pages that are out of view and prefetch radius */
+    @NonNull
+    public Range[] getGonePageRanges(@NonNull Range nearPages) {
+        Range allPages = new Range(0, mPaginationModel.getSize() - 1);
+        return allPages.minus(nearPages);
+    }
+
+    /** Returns the range of pages near the visible pages that are invisible to the view port */
+    @NonNull
+    public Range[] getInvisibleNearPageRanges(@NonNull Range nearPages) {
+        return nearPages.minus(mVisiblePages);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
new file mode 100644
index 0000000..7a7f55c
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageSelectionValueObserver.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import android.content.Context;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.PaginationUtils;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class PageSelectionValueObserver implements ObservableValue.ValueObserver<PageSelection> {
+    private final PaginatedView mPaginatedView;
+    private final PaginationModel mPaginationModel;
+    private final PageViewFactory mPageViewFactory;
+    private Context mContext;
+
+    PageSelectionValueObserver(PaginatedView paginatedView, PaginationModel paginationModel,
+            PageViewFactory pageViewFactory, Context context) {
+        mPaginatedView = paginatedView;
+        mPaginationModel = paginationModel;
+        mPageViewFactory = pageViewFactory;
+        mContext = context;
+    }
+
+    @Override
+    public void onChange(PageSelection oldSelection, PageSelection newSelection) {
+        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+            getPage(oldSelection.getPage()).getPageView().setOverlay(null);
+        }
+
+        Range visiblePageRange =
+                mPaginatedView.getPageRangeHandler().getVisiblePages();
+        if (newSelection != null && visiblePageRange.contains(
+                newSelection.getPage())) {
+            ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
+                    newSelection.getPage(),
+                    PaginationUtils.getPageElevationInPixels(mContext),
+                    mPaginationModel.getPageSize(newSelection.getPage())))
+                    .setOverlay(new PdfHighlightOverlay(newSelection));
+        }
+    }
+
+    private boolean isPageCreated(int pageNum) {
+        return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+    }
+
+    private PageViewFactory.PageView getPage(int pageNum) {
+        return mPaginatedView.getViewAt(pageNum);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
index 0d0730f..6a802cd 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageTouchListener.java
@@ -20,25 +20,28 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.pdf.models.SelectionBoundary;
 import androidx.pdf.util.GestureTracker;
 import androidx.pdf.viewer.loader.PdfLoader;
 
 /** Gesture listener for PageView's handling of tap and long press. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 class PageTouchListener extends GestureTracker.GestureHandler {
 
     private final PageViewFactory.PageView mPageView;
 
     private final PdfLoader mPdfLoader;
 
-    private final PdfViewer.PageTouchHandler mPageTouchHandler;
+
+    private final SingleTapHandler mSingleTapHandler;
 
     PageTouchListener(@NonNull PageViewFactory.PageView pageView,
             @NonNull PdfLoader pdfLoader,
-            @NonNull PdfViewer.PageTouchHandler pageTouchHandler) {
+            @NonNull SingleTapHandler singleTapHandler) {
         this.mPageView = pageView;
         this.mPdfLoader = pdfLoader;
-        this.mPageTouchHandler = pageTouchHandler;
+        this.mSingleTapHandler = singleTapHandler;
     }
 
     @Override
@@ -48,7 +51,8 @@
 
     @Override
     public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
-        return mPageTouchHandler.handleSingleTapNoFormFilling(e, mPageView.getPageView());
+        mSingleTapHandler.handleSingleTapConfirmedEventOnPage(e, mPageView.getPageView());
+        return true;
     }
 
     @Override
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
index efd550f..f328ed9 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PageViewFactory.java
@@ -29,7 +29,6 @@
 import androidx.pdf.models.LinkRects;
 import androidx.pdf.util.Accessibility;
 import androidx.pdf.util.GestureTracker;
-import androidx.pdf.util.ObservableValue;
 import androidx.pdf.util.TileBoard;
 import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.pdf.widget.MosaicView;
@@ -53,16 +52,19 @@
     private final Context mContext;
     private final PdfLoader mPdfLoader;
     private final PaginatedView mPaginatedView;
-    private final ObservableValue<ZoomView.ZoomScroll> mZoomScroll;
+    private final ZoomView mZoomView;
+    private final SingleTapHandler mSingleTapHandler;
 
     public PageViewFactory(@NonNull Context context,
             @NonNull PdfLoader pdfLoader,
             @NonNull PaginatedView paginatedView,
-            @NonNull ZoomView zoomView) {
+            @NonNull ZoomView zoomView,
+            @NonNull SingleTapHandler singleTapHandler) {
         this.mContext = context;
         this.mPdfLoader = pdfLoader;
         this.mPaginatedView = paginatedView;
-        this.mZoomScroll = zoomView.zoomScroll();
+        this.mZoomView = zoomView;
+        this.mSingleTapHandler = singleTapHandler;
     }
 
     /**
@@ -98,20 +100,21 @@
         void clearAll();
     }
 
-    /** Returns an instance of {@link PageView}. If the view is already created and added to the
-     *  {@link PaginatedView} then it will be returned from that list else a new instance will be
-     *  created. */
+    /**
+     * Returns an instance of {@link PageView}. If the view is already created and added to the
+     * {@link PaginatedView} then it will be returned from that list else a new instance will be
+     * created.
+     */
     @NonNull
     public PageView getOrCreatePageView(int pageNum,
             int pageElevationInPixels,
-            @NonNull Dimensions pageDimensions,
-            @NonNull PdfViewer.PageTouchHandler handler) {
+            @NonNull Dimensions pageDimensions) {
         PageView pageView = mPaginatedView.getViewAt(pageNum);
         if (pageView != null) {
             return pageView;
         }
 
-        return createAndSetupPageView(pageNum, pageElevationInPixels, pageDimensions, handler);
+        return createAndSetupPageView(pageNum, pageElevationInPixels, pageDimensions);
     }
 
     /**
@@ -126,9 +129,10 @@
         final MosaicView.BitmapSource bitmapSource = createBitmapSource(pageNum);
         final PageMosaicView pageMosaicView =
                 new PageMosaicView(mContext, pageNum, pageSize, bitmapSource,
-                        TileBoard.DEFAULT_RECYCLER);
+                        TileBoard.DEFAULT_RECYCLER, mPdfLoader, mPaginatedView.getSelectionModel(),
+                        mPaginatedView.getSearchModel());
         if (isTouchExplorationEnabled(mContext)) {
-            final PageLinksView pageLinksView = new PageLinksView(mContext, mZoomScroll);
+            final PageLinksView pageLinksView = new PageLinksView(mContext, mZoomView.zoomScroll());
 
             return new AccessibilityPageWrapper(
                     mContext, pageNum, pageMosaicView, pageLinksView);
@@ -168,8 +172,7 @@
     @NonNull
     protected PageView createAndSetupPageView(int pageNum,
             int pageElevationInPixels,
-            @NonNull Dimensions pageDimensions,
-            @NonNull PdfViewer.PageTouchHandler handler) {
+            @NonNull Dimensions pageDimensions) {
         PageView pageView =
                 createPageView(
                         pageNum,
@@ -178,8 +181,9 @@
         mPaginatedView.addView(pageView);
 
         GestureTracker gestureTracker = new GestureTracker(mContext);
+        gestureTracker.setDelegateHandler(new PageTouchListener(pageView, mPdfLoader,
+                mSingleTapHandler));
         pageView.asView().setOnTouchListener(gestureTracker);
-        gestureTracker.setDelegateHandler(new PageTouchListener(pageView, mPdfLoader, handler));
 
         PageMosaicView pageMosaicView = pageView.getPageView();
         // Setting Elevation only works if there is a background color.
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 35bd5b8..f1ae9f2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -25,8 +25,13 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Range;
+import androidx.pdf.util.PaginationUtils;
 import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.ThreadUtils;
 import androidx.pdf.viewer.PageViewFactory.PageView;
+import androidx.pdf.viewer.loader.PdfLoader;
+import androidx.pdf.widget.ZoomView;
 
 import java.util.AbstractList;
 import java.util.List;
@@ -46,16 +51,72 @@
     /** Maps the current child views to pages. */
     private final SparseArray<PageView> mPageViews = new SparseArray<>();
 
+    private PaginationModel mPaginationModel;
+
+    private PageRangeHandler mPageRangeHandler;
+
+    private PdfSelectionModel mSelectionModel;
+
+    private SearchModel mSearchModel;
+
+    private PdfLoader mPdfLoader;
+
+    private PageViewFactory mPageViewFactory;
+
     public PaginatedView(@NonNull Context context) {
-        super(context);
+        this(context, null);
     }
 
-    public PaginatedView(@NonNull Context context, @NonNull AttributeSet attrs) {
-        super(context, attrs);
+    public PaginatedView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
     }
 
-    public PaginatedView(@NonNull Context context, @NonNull AttributeSet attrs, int defStyle) {
+    public PaginatedView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        mPaginationModel = new PaginationModel();
+        mPageRangeHandler = new PageRangeHandler(mPaginationModel);
+    }
+
+    @NonNull
+    public PaginationModel getPaginationModel() {
+        return mPaginationModel;
+    }
+
+    @NonNull
+    public PageRangeHandler getPageRangeHandler() {
+        return mPageRangeHandler;
+    }
+
+    @NonNull
+    public PdfSelectionModel getSelectionModel() {
+        return mSelectionModel;
+    }
+
+    public void setSelectionModel(
+            @NonNull PdfSelectionModel selectionModel) {
+        mSelectionModel = selectionModel;
+    }
+
+    @NonNull
+    public SearchModel getSearchModel() {
+        return mSearchModel;
+    }
+
+    public void setSearchModel(@NonNull SearchModel searchModel) {
+        mSearchModel = searchModel;
+    }
+
+    public void setPdfLoader(@NonNull PdfLoader pdfLoader) {
+        mPdfLoader = pdfLoader;
+    }
+
+    @NonNull
+    public PageViewFactory getPageViewFactory() {
+        return mPageViewFactory;
+    }
+
+    public void setPageViewFactory(@NonNull PageViewFactory pageViewFactory) {
+        mPageViewFactory = pageViewFactory;
     }
 
     /** Instantiate a page of this pageView into a child pageView. */
@@ -200,4 +261,165 @@
         // laid out already for this viewArea.
         onLayout(false, getLeft(), getTop(), getRight(), getBottom());
     }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (getVisibility() == View.VISIBLE) {
+            mPageRangeHandler.adjustMaxPageToUpperVisibleRange();
+            if (getChildCount() > 0) {
+                for (PageMosaicView page : getChildViews()) {
+                    page.clearTiles();
+                    if (mPdfLoader != null) {
+                        mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mPageRangeHandler.setVisiblePages(null);
+    }
+
+    /**
+     * Refreshes the page range for the visible area.
+     */
+    public void refreshPageRangeInVisibleArea(@NonNull ZoomView.ZoomScroll zoomScroll,
+            int parentViewHeight) {
+        mPageRangeHandler.refreshVisiblePageRange(zoomScroll.scrollY, zoomScroll.zoom,
+                parentViewHeight);
+
+        mPageRangeHandler.adjustMaxPageToUpperVisibleRange();
+    }
+
+    /** Cancels the background jobs for the disappeared pages and optionally clears the views */
+    public void handleGonePages(boolean clearViews) {
+        Range nearPages = mPageRangeHandler.getNearPagesToVisibleRange();
+        Range[] gonePages = mPageRangeHandler.getGonePageRanges(nearPages);
+        for (Range pages : gonePages) {
+            // Keep Views around for now, we'll clear them in step (4) if applicable.
+            clearPages(pages, clearViews);
+        }
+    }
+
+    /** Computes the invisible page range and loads them */
+    public void loadInvisibleNearPageRange(
+            float stableZoom) {
+        Range nearPages = mPageRangeHandler.getNearPagesToVisibleRange();
+        Range[] invisibleNearPages = mPageRangeHandler.getInvisibleNearPageRanges(nearPages);
+
+        for (Range pages : invisibleNearPages) {
+            loadPageRange(pages, stableZoom);
+        }
+    }
+
+    /**
+     * Creates the page views for the visible page range.
+     *
+     * @return true if any new page was created else false
+     */
+    public boolean createPageViewsForVisiblePageRange() {
+        boolean requiresLayoutPass = false;
+        for (int pageNum : mPageRangeHandler.getVisiblePages()) {
+            if (getViewAt(pageNum) == null) {
+                mPageViewFactory.getOrCreatePageView(pageNum,
+                        PaginationUtils.getPageElevationInPixels(getContext()),
+                        mPaginationModel.getPageSize(pageNum));
+                requiresLayoutPass = true;
+            }
+        }
+        return requiresLayoutPass;
+    }
+
+    /**  */
+    public void refreshVisiblePages(boolean requiresLayoutPass,
+            @NonNull Viewer.ViewState viewState,
+            float stableZoom) {
+        if (requiresLayoutPass) {
+            refreshPagesAfterLayout(viewState, mPageRangeHandler.getVisiblePages(),
+                    stableZoom);
+        } else {
+            refreshPages(mPageRangeHandler.getVisiblePages(), stableZoom);
+        }
+        handleGonePages(/* clearViews= */ true);
+    }
+
+    /**  */
+    public void refreshVisibleTiles(boolean requiresLayoutPass,
+            @NonNull Viewer.ViewState viewState) {
+        if (requiresLayoutPass) {
+            refreshTilesAfterLayout(viewState, mPageRangeHandler.getVisiblePages());
+        } else {
+            refreshTiles(mPageRangeHandler.getVisiblePages());
+        }
+    }
+
+    private void clearPages(Range pages, boolean clearViews) {
+        for (int page : pages) {
+            // Don't cancel search - search results for the current search are always useful,
+            // even for pages we can't see right now. Form filling operations should always
+            // be executed against the document, even if the user has scrolled away from the page.
+            mPdfLoader.cancelExceptSearchAndFormFilling(page);
+            if (clearViews) {
+                removeViewAt(page);
+            }
+        }
+    }
+
+    private void loadPageRange(Range pages,
+            float stableZoom) {
+        for (int page : pages) {
+            mPdfLoader.cancelAllTileBitmaps(page);
+            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+                    page,
+                    PaginationUtils.getPageElevationInPixels(getContext()),
+                    mPaginationModel.getPageSize(page));
+            pageView.clearTiles();
+            pageView.requestFastDrawAtZoom(stableZoom);
+            pageView.refreshPageContentAndOverlays();
+        }
+    }
+
+    private void refreshPages(Range pages, float stableZoom) {
+        for (int page : pages) {
+            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+                    page,
+                    PaginationUtils.getPageElevationInPixels(getContext()),
+                    mPaginationModel.getPageSize(page));
+            pageView.requestDrawAtZoom(stableZoom);
+            pageView.refreshPageContentAndOverlays();
+        }
+    }
+
+    private void refreshPagesAfterLayout(Viewer.ViewState viewState, Range pages,
+            float stableZoom) {
+        ThreadUtils.postOnUiThread(
+                () -> {
+                    if (viewState != Viewer.ViewState.NO_VIEW) {
+                        refreshPages(pages, stableZoom);
+                    }
+                });
+    }
+
+    private void refreshTiles(Range pages) {
+        for (int page : pages) {
+            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+                    page,
+                    PaginationUtils.getPageElevationInPixels(getContext()),
+                    mPaginationModel.getPageSize(page));
+            pageView.requestTiles();
+        }
+    }
+
+    private void refreshTilesAfterLayout(Viewer.ViewState viewState, Range pages) {
+        ThreadUtils.postOnUiThread(
+                () -> {
+                    if (viewState != Viewer.ViewState.NO_VIEW) {
+                        refreshTiles(pages);
+                    }
+                });
+    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index 7d8e888..f0205c3 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -26,12 +26,10 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver.OnScrollChangedListener;
@@ -62,24 +60,20 @@
 import androidx.pdf.find.MatchCount;
 import androidx.pdf.models.Dimensions;
 import androidx.pdf.models.GotoLink;
-import androidx.pdf.models.GotoLinkDestination;
 import androidx.pdf.models.LinkRects;
 import androidx.pdf.models.MatchRects;
 import androidx.pdf.models.PageSelection;
 import androidx.pdf.util.AnnotationUtils;
 import androidx.pdf.util.CycleRange;
-import androidx.pdf.util.ExternalLinks;
 import androidx.pdf.util.ObservableValue;
 import androidx.pdf.util.ObservableValue.ValueObserver;
 import androidx.pdf.util.Preconditions;
 import androidx.pdf.util.Screen;
-import androidx.pdf.util.StrictModeUtils;
 import androidx.pdf.util.ThreadUtils;
 import androidx.pdf.util.TileBoard;
 import androidx.pdf.util.TileBoard.TileInfo;
 import androidx.pdf.util.Toaster;
 import androidx.pdf.util.Uris;
-import androidx.pdf.util.ZoomUtils;
 import androidx.pdf.viewer.PageViewFactory.PageView;
 import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.pdf.viewer.loader.PdfLoaderCallbacks;
@@ -96,8 +90,6 @@
 import com.google.android.material.snackbar.Snackbar;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 
-import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 
 /**
@@ -125,22 +117,16 @@
  *       connected.
  * </ol>
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @SuppressWarnings({"UnusedMethod", "UnusedVariable"})
 public class PdfViewer extends LoadingViewer implements FastScrollContentModel {
 
     private static final String TAG = "PdfViewer";
 
-    @NonNull
-    @Override
-    protected String getLogTag() {
-        return TAG;
-    }
-
     /** {@link View#setElevation(float)} value for PDF Pages (API 21+). */
     private static final int PAGE_ELEVATION_DP = 2;
 
-    /** Key for saving {@link #mPageLayoutReach} in bundles. */
+    /** Key for saving page layout reach in bundles. */
     private static final String KEY_LAYOUT_REACH = "plr";
 
     private static final String KEY_SPACE_LEFT = "leftSpace";
@@ -150,9 +136,6 @@
     private static final String KEY_QUIT_ON_ERROR = "quitOnError";
     private static final String KEY_EXIT_ON_CANCEL = "exitOnCancel";
 
-    /** Key to save/retrieve {@link #mEditingAuthorized} from Bundle. */
-    private static final String KEY_EDITING_AUTHORIZED = "editingAuthorized";
-
     private static Screen sScreen;
 
     /** Single access to the PDF document: loads contents asynchronously (bitmaps, text,...) */
@@ -166,7 +149,7 @@
     public final PdfLoaderCallbacks mPdfLoaderCallbacks;
 
     /** Observer of the page position that controls loading of relevant PDF assets. */
-    private final ValueObserver<ZoomScroll> mZoomScrollObserver;
+    private ValueObserver<ZoomScroll> mZoomScrollObserver;
 
     /** Observer to be set when the view is created. */
     @Nullable
@@ -174,23 +157,6 @@
 
     private Object mScrollPositionObserverKey;
 
-    /** The number of pages of this PDF, set to -1 when not available. */
-    private int mNumPages = -1;
-
-    /** The range of currently visible pages. */
-    private Range mVisiblePages;
-
-    /** The highest number page reached. */
-    private int mMaxPage = -1;
-
-    private int mInitialPageLayoutReach = 4;
-
-    /** The number of pages that have been laid out in the document. */
-    private int mPageLayoutReach;
-
-    /** The last stable zoom: we only re-draw bitmaps at stable zoom (not during a gesture). */
-    private float mStableZoom;
-
     private ZoomView mZoomView;
 
     private PaginatedView mPaginatedView;
@@ -201,15 +167,18 @@
     private SearchModel mSearchModel;
     private PdfSelectionModel mSelectionModel;
     private PdfSelectionHandles mSelectionHandles;
-    private final ValueObserver<String> mSearchQueryObserver;
-    private final ValueObserver<SelectedMatch> mSelectedMatchObserver;
-    private final ValueObserver<PageSelection> mSelectionObserver;
-    private final ValueObserver<Integer> mFastscrollerPositionObserver;
+
+    private ValueObserver<String> mSearchQueryObserver;
+    private ValueObserver<Integer> mFastscrollerPositionObserver;
+    private ValueObserver<SelectedMatch> mSelectedMatchObserver;
+    private ValueObserver<PageSelection> mSelectionObserver;
     private Object mFastscrollerPositionObserverKey;
     private FastScrollView mFastScrollView;
     private ProgressBar mLoadingSpinner;
 
     private boolean mDocumentLoaded = false;
+    private boolean mIsAnnotationIntentResolvable = false;
+
     /**
      * After the document content is saved over the original in InkActivity, we set this bit to true
      * so we know to callwhen the new document content is loaded.
@@ -224,15 +193,10 @@
     @Nullable
     private SettableFutureValue<Boolean> mSaveAsCallback;
 
-    // Base padding for ZoomView in px as set in saveZoomViewBasePadding().
-    private Rect mZoomViewBasePadding = new Rect();
-    private boolean mZoomViewBasePaddingSaved;
     private Snackbar mSnackbar;
-    private boolean mWaitingOnSelectionToCreateInlineComment;
-    private boolean mEditingAuthorized;
 
-    /** Only interact with Queue on the main thread. */
-    private final List<OnDimensCallback> mDimensCallbackQueue = new ArrayList<>();
+    private LayoutHandler mLayoutHandler;
+
     private Uri mLocalUri;
     private FrameLayout mPdfViewer;
 
@@ -242,11 +206,7 @@
 
     private PageViewFactory mPageViewFactory;
 
-    /** Callback is called everytime dimensions for a page have loaded. */
-    private interface OnDimensCallback {
-        /** Return true to continue receiving callbacks, else false. */
-        boolean onDimensLoaded(int pageNum);
-    }
+    private SingleTapHandler mSingleTapHandler;
 
     public PdfViewer() {
         super(SELF_MANAGED_CONTENTS);
@@ -289,15 +249,16 @@
     @NonNull
     @SuppressLint("InflateParams")
     @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container,
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
             @Nullable Bundle savedState) {
         super.onCreateView(inflater, container, savedState);
-        mPaginationModel = new PaginationModel();
 
         mPdfViewer = (FrameLayout) inflater.inflate(R.layout.pdf_viewer_container, container,
                 false);
         mFindInFileView = mPdfViewer.findViewById(R.id.search);
         mFastScrollView = mPdfViewer.findViewById(R.id.fast_scroll_view);
+        mPaginatedView = mFastScrollView.findViewById(R.id.pdf_view);
+        mPaginationModel = mPaginatedView.getPaginationModel();
 
         mZoomView = mFastScrollView.findViewById(R.id.zoom_view);
         mZoomView.setStraightenVerticalScroll(true);
@@ -312,14 +273,12 @@
         // predictable. An alternative that doesn't require id is to rely on this Fragment's
         // onSaveInstanceState().
         mZoomView.setId(getId() * 100);
-        mPaginatedView = mFastScrollView.findViewById(R.id.pdf_view);
-
-        mVisiblePages = new Range();
-        mPageLayoutReach = 0;
 
         mPageIndicator = new PageIndicator(getActivity(), mFastScrollView);
         applyReservedSpace();
-        adjustZoomViewMargins();
+        mZoomView.adjustZoomViewMargins();
+        mFastscrollerPositionObserver =
+                new FastScrollPositionValueObserver(mFastScrollView, mPageIndicator);
         mFastscrollerPositionObserver.onChange(null, mFastScrollView.getScrollerPositionY().get());
         mFastscrollerPositionObserverKey =
                 mFastScrollView.getScrollerPositionY().addObserver(mFastscrollerPositionObserver);
@@ -332,7 +291,6 @@
         mFastScrollView.setId(getId() * 10);
 
         mLoadingSpinner = mFastScrollView.findViewById(R.id.progress_indicator);
-
         setUpEditFab();
 
         return mPdfViewer;
@@ -350,7 +308,7 @@
 
     private void applyReservedSpace() {
         if (getArguments().containsKey(KEY_SPACE_TOP)) {
-            saveZoomViewBasePadding();
+            mZoomView.saveZoomViewBasePadding();
             int left = getArguments().getInt(KEY_SPACE_LEFT, 0);
             int top = getArguments().getInt(KEY_SPACE_TOP, 0);
             int right = getArguments().getInt(KEY_SPACE_RIGHT, 0);
@@ -358,11 +316,7 @@
 
             mPageIndicator.getView().setTranslationX(-right);
 
-            mZoomView.setPadding(
-                    mZoomViewBasePadding.left + left,
-                    mZoomViewBasePadding.top + top,
-                    mZoomViewBasePadding.right + right,
-                    mZoomViewBasePadding.bottom + bottom);
+            mZoomView.setPaddingWithBase(left, top, right, bottom);
 
             // Adjust the scroll bar to also include the same padding.
             mFastScrollView.setScrollbarMarginTop(mZoomView.getPaddingTop());
@@ -371,62 +325,9 @@
         }
     }
 
-    /**
-     * Saves the padding set on {@link ZoomView} following initial inflation from XML.
-     *
-     * <p>This does not have to be called immediately following inflation but <i>must</i> be called
-     * before any methods change the padding on {@link ZoomView}.
-     *
-     * <p>This can be used by methods that need to set padding to (base padding + some other
-     * dimension). If these values were obtained directly from {@link ZoomView} or this method was
-     * allowed to execute multiple times it could result in padding expanding continually.
-     */
-    private void saveZoomViewBasePadding() {
-        if (mZoomView == null || mZoomViewBasePaddingSaved) {
-            return;
-        }
-
-        mZoomViewBasePadding =
-                new Rect(
-                        mZoomView.getPaddingLeft(),
-                        mZoomView.getPaddingTop(),
-                        mZoomView.getPaddingRight(),
-                        mZoomView.getPaddingBottom());
-
-        mZoomViewBasePadding.top +=
-                getResources().getDimensionPixelSize(R.dimen.viewer_doc_additional_top_offset);
-
-        mZoomViewBasePaddingSaved = true;
-    }
-
-    /**
-     * Adjusts the horizontal margins (left and right padding) of the ZoomView based on the
-     * screen width to optimize the display of PDF content.
-     *
-     * This method applies different margin values depending on the screen size:
-     * - For screens with a screen width of 840dp or greater, a larger margin is applied
-     * to enhance readability on larger displays.
-     * - For screens with a screen width < 840dp, no margin is used to
-     * maximize the use of available space.
-     *
-     * This dynamic adjustment is achieved through the use of resource qualifiers (values-w840dp)
-     * that define different margin values for different screen sizes.
-     *
-     * Note: This method does not affect the top or bottom padding of the ZoomView.
-     */
-    private void adjustZoomViewMargins() {
-        int margin = getResources().getDimensionPixelSize(R.dimen.viewer_doc_padding_x);
-
-        mZoomView.setPadding(margin,
-                mZoomView.getPaddingTop(),
-                margin,
-                mZoomView.getPaddingBottom());
-    }
-
     @Override
     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
-        mZoomView.zoomScroll().addObserver(mZoomScrollObserver);
         if (mPendingScrollPositionObserver != null) {
             mScrollPositionObserverKey = mZoomView.zoomScroll().addObserver(
                     mPendingScrollPositionObserver);
@@ -437,9 +338,8 @@
     @Override
     protected void onContentsAvailable(@NonNull DisplayData contents, @Nullable Bundle savedState) {
         mFileData = contents;
+        mLocalUri = contents.getUri();
 
-        // TODO: StrictMode- disk read 58ms.
-        int lengthMb = StrictModeUtils.bypassAndReturn(() -> (int) (contents.length() >> 20));
         createContentModel(
                 PdfLoader.create(
                         getActivity().getApplicationContext(),
@@ -447,11 +347,40 @@
                         TileBoard.DEFAULT_RECYCLER,
                         mPdfLoaderCallbacks,
                         false));
+        mLayoutHandler = new LayoutHandler(mPdfLoader);
+        mZoomView.setPdfSelectionModel(mSelectionModel);
+        mPaginatedView.setSelectionModel(mSelectionModel);
+        mPaginatedView.setSearchModel(mSearchModel);
+        mPaginatedView.setPdfLoader(mPdfLoader);
+
+        mSearchQueryObserver =
+                new SearchQueryObserver(mPaginatedView);
+        mSearchModel.query().addObserver(mSearchQueryObserver);
+
+        mZoomScrollObserver =
+                new ZoomScrollValueObserver(mZoomView, mPaginatedView,
+                        mLayoutHandler, mAnnotationButton, mFindInFileView, mPageIndicator,
+                        mFastScrollView, mIsAnnotationIntentResolvable, mViewState);
+        mZoomView.zoomScroll().addObserver(mZoomScrollObserver);
+        mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton,
+                mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler);
+        mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
+                mPaginatedView, mZoomView, mSingleTapHandler);
+        mPaginatedView.setPageViewFactory(mPageViewFactory);
+
+        mSelectionObserver =
+                new PageSelectionValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+                        requireContext());
+        mSelectionModel.selection().addObserver(mSelectionObserver);
+
+        mSelectedMatchObserver =
+                new SelectedMatchValueObserver(mPaginatedView, mPaginationModel, mPageViewFactory,
+                        mZoomView, mLayoutHandler, requireContext());
+        mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
 
         if (savedState != null) {
             int layoutReach = savedState.getInt(KEY_LAYOUT_REACH);
-            mEditingAuthorized = savedState.getBoolean(KEY_EDITING_AUTHORIZED);
-            mInitialPageLayoutReach = Math.max(mInitialPageLayoutReach, layoutReach);
+            mLayoutHandler.setInitialPageLayoutReachWithMax(layoutReach);
         }
     }
 
@@ -465,9 +394,6 @@
             mPdfLoader.reconnect();
         }
 
-        mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
-                mPaginatedView, mZoomView);
-
         if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
             loadPageAssets(mZoomView.zoomScroll().get());
         }
@@ -475,35 +401,19 @@
 
     @Override
     public void onExit() {
-        if (mVisiblePages != null && mVisiblePages.getLast() > mMaxPage) {
-            mMaxPage = mVisiblePages.getLast();
-        }
-
         super.onExit();
         if (!mDocumentLoaded && mPdfLoader != null) {
             // e.g. a password-protected pdf that wasn't loaded.
             mPdfLoader.disconnect();
         }
-
-        if (mPaginatedView != null && mPaginatedView.getChildCount() > 0) {
-            for (PageMosaicView page : mPaginatedView.getChildViews()) {
-                page.clearTiles();
-                if (mPdfLoader != null) {
-                    mPdfLoader.cancelAllTileBitmaps(page.getPageNum());
-                }
-            }
-        }
     }
 
     private void createContentModel(PdfLoader pdfLoader) {
         this.mPdfLoader = pdfLoader;
 
         mSearchModel = new SearchModel(pdfLoader);
-        mSearchModel.query().addObserver(mSearchQueryObserver);
-        mSearchModel.selectedMatch().addObserver(mSelectedMatchObserver);
 
         mSelectionModel = new PdfSelectionModel(pdfLoader);
-        mSelectionModel.selection().addObserver(mSelectionObserver);
 
         mSelectionHandles = new PdfSelectionHandles(mSelectionModel, mZoomView, mPaginatedView);
 
@@ -544,6 +454,8 @@
             if (mScrollPositionObserverKey != null) {
                 mZoomView.zoomScroll().removeObserver(mScrollPositionObserverKey);
             }
+            mZoomView.setZoomViewBasePadding(new Rect());
+            mZoomView.setZoomViewBasePaddingSaved(false);
             mZoomView = null;
         }
 
@@ -552,17 +464,12 @@
             mPaginationModel.removeObserver(mPaginatedView);
             mPaginatedView = null;
         }
-        // Clears the model so we can start fresh if we rebuild views.
-        mPaginationModel = new PaginationModel();
-        mVisiblePages = null;
 
         if (mPdfLoader != null) {
             mPdfLoader.cancelAll();
             mPdfLoader.disconnect();
             mDocumentLoaded = false;
         }
-        mZoomViewBasePadding = new Rect();
-        mZoomViewBasePaddingSaved = false;
         super.destroyView();
     }
 
@@ -588,28 +495,13 @@
 
     @Override
     public void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putInt(KEY_LAYOUT_REACH, mPageLayoutReach);
-
-        outState.putBoolean(KEY_EDITING_AUTHORIZED, mEditingAuthorized);
+        outState.putInt(KEY_LAYOUT_REACH, mLayoutHandler.getPageLayoutReach());
     }
 
     @Override
     public void onConfigurationChanged(@NonNull Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        adjustZoomViewMargins();
-    }
-
-    @Override
-    public long getContentLength() {
-        return getPageCount();
-    }
-
-    @Override
-    public int getViewProgress() {
-        if (mMaxPage > 0) {
-            return (int) (((double) mMaxPage / getPageCount() * 100) * PROGRESS_SCALER);
-        }
-        return -1;
+        mZoomView.adjustZoomViewMargins();
     }
 
     /**
@@ -629,6 +521,9 @@
         showSpinner();
         fetchFile(fileUri);
         mLocalUri = fileUri;
+        mIsAnnotationIntentResolvable = AnnotationUtils.resolveAnnotationIntent(requireContext(),
+                mLocalUri);
+        mSingleTapHandler.setAnnotationIntentResolvable(mIsAnnotationIntentResolvable);
     }
 
     private void validateFileUri(Uri fileUri) {
@@ -708,45 +603,6 @@
         postEnter();
     }
 
-    private int getPageCount() {
-        return mNumPages;
-    }
-
-    /** Returns the page currently roughly centered in the view. */
-    public int getViewingPage() {
-        return (mVisiblePages != null) ? (mVisiblePages.getFirst() + mVisiblePages.getLast()) / 2
-                : 0;
-    }
-
-    /**
-     * Lay out some pages up to some distant page. Not guaranteed to lay out any pages: maybe all
-     * pages, or at least enough pages, are already laid out.
-     */
-    private void maybeLayoutPages(int current) {
-        int peekAhead = Math.min(current + 2, 100);
-        int distantPage = Math.max(current + peekAhead, mInitialPageLayoutReach);
-        layoutPages(distantPage);
-    }
-
-    /**
-     * Lays out all the pages until {@code untilPage}, or equivalently so that {@code untilPage}s
-     * are laid out. So calling with {@code untilPage = 10} will ensure pages 0-9 are laid out.
-     *
-     * @param untilPage The upper limit of the range of pages to be laid out. Cropped to the
-     *                  number of pages of the document if this number was larger.
-     */
-    private void layoutPages(int untilPage) {
-        if (mPdfLoader == null) {
-            return;
-        }
-        int lastPage = Math.min(untilPage, getPageCount());
-        int requestLayoutPage = mPageLayoutReach;
-        while (requestLayoutPage < lastPage) {
-            mPdfLoader.loadPageDimensions(requestLayoutPage);
-            requestLayoutPage++;
-        }
-    }
-
     private boolean isPageCreated(int pageNum) {
         return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
     }
@@ -755,16 +611,12 @@
         return mPaginatedView.getViewAt(pageNum);
     }
 
-    private Range allPages() {
-        return new Range(0, mPaginationModel.getSize() - 1);
-    }
-
     private void lookAtSelection(SelectedMatch selection) {
         if (selection == null || selection.isEmpty()) {
             return;
         }
         if (selection.getPage() >= mPaginationModel.getSize()) {
-            layoutPages(selection.getPage() + 1);
+            mLayoutHandler.layoutPages(selection.getPage() + 1);
             return;
         }
         Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
@@ -775,148 +627,38 @@
         PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
                 selection.getPage(),
                 sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                mPaginationModel.getPageSize(selection.getPage()),
-                new PageTouchHandler());
+                mPaginationModel.getPageSize(selection.getPage()));
         pageView.setOverlay(selection.getOverlay());
     }
 
     private void loadPageAssets(ZoomScroll position) {
-        Range oldVisiblePages = mVisiblePages;
-
-        // 1. Refresh visible pages and view area.
-        mVisiblePages = computeVisibleRange(position);
-
-        if (mVisiblePages.getLast() > mMaxPage) {
-            mMaxPage = mVisiblePages.getLast();
-        }
-
         // Change the resolution of the bitmaps only when a gesture is not in progress.
-        if (position.stable || mStableZoom == 0) {
-            mStableZoom = position.zoom;
+        if (position.stable || mZoomView.getStableZoom() == 0) {
+            mZoomView.setStableZoom(position.zoom);
         }
 
         mPaginationModel.setViewArea(mZoomView.getVisibleAreaInContentCoords());
-
-        Range allPages = allPages();
-        int prefetchRadius = 1; // Note: could make this variable depending on resolution.
-        Range nearPages = expandRange(mVisiblePages, prefetchRadius, allPages);
-
-        // 2. Release pages that we don't need anymore.
-        Range[] gonePages = allPages.minus(nearPages);
-        for (Range pages : gonePages) {
-            // Keep Views around for now, we'll clear them in step (4) if applicable.
-            clearPages(pages, false);
-        }
-
-        // 3. Bring minimal service to pages that we might need very soon
-        Range[] invisibleNearPages = nearPages.minus(mVisiblePages);
-        for (Range pages : invisibleNearPages) {
-            loadPageOnly(pages);
-        }
+        mPaginatedView.refreshPageRangeInVisibleArea(position, mZoomView.getHeight());
+        mPaginatedView.handleGonePages(/* clearViews= */ false);
+        mPaginatedView.loadInvisibleNearPageRange(mZoomView.getStableZoom());
 
         // The step (4) below requires page Views to be created and laid out. So we create them here
         // and set this flag if that operation needs to wait for a layout pass.
-        boolean requiresLayoutPass = false;
-        for (int pageNum : mVisiblePages) {
-            if (mPaginatedView.getViewAt(pageNum) == null) {
-                mPageViewFactory.getOrCreatePageView(pageNum,
-                        sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                        mPaginationModel.getPageSize(pageNum),
-                        new PageTouchHandler());
-                requiresLayoutPass = true;
-            }
-        }
+        boolean requiresLayoutPass = mPaginatedView.createPageViewsForVisiblePageRange();
 
         // 4. Refresh tiles and/or full pages.
         if (position.stable) {
             // Perform a full refresh on all visible pages
-            if (requiresLayoutPass) {
-                refreshPagesAfterLayout(mVisiblePages);
-            } else {
-                refreshPages(mVisiblePages);
-            }
-            for (Range pages : gonePages) {
-                clearPages(pages, true);
-            }
-        } else if (mStableZoom == position.zoom) {
+            mPaginatedView.refreshVisiblePages(requiresLayoutPass, viewState().get(),
+                    mZoomView.getStableZoom());
+            mPaginatedView.handleGonePages(/* clearViews= */ true);
+        } else if (mZoomView.getStableZoom() == position.zoom) {
             // Just load a few more tiles in case of tile-scroll
-            if (requiresLayoutPass) {
-                refreshTilesAfterLayout(mVisiblePages);
-            } else {
-                refreshTiles(mVisiblePages);
-            }
+            mPaginatedView.refreshVisibleTiles(requiresLayoutPass, viewState().get());
         }
 
-        maybeLayoutPages(mVisiblePages.getLast());
-    }
-
-    private void refreshTiles(Range pages) {
-        for (int page : pages) {
-            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
-                    page,
-                    sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                    mPaginationModel.getPageSize(page),
-                    new PageTouchHandler());
-            pageView.requestTiles();
-        }
-    }
-
-    private void refreshTilesAfterLayout(final Range pages) {
-        ThreadUtils.postOnUiThread(
-                () -> {
-                    if (viewState().get() != ViewState.NO_VIEW) {
-                        refreshTiles(pages);
-                    }
-                });
-    }
-
-    private void loadPageOnly(Range pages) {
-        for (int page : pages) {
-            mPdfLoader.cancelAllTileBitmaps(page);
-            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
-                    page,
-                    sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                    mPaginationModel.getPageSize(page),
-                    new PageTouchHandler());
-            pageView.clearTiles();
-            pageView.requestFastDrawAtZoom(mStableZoom);
-            loadVisiblePageText(page);
-            maybeLoadFormAccessibilityInfo(page);
-        }
-    }
-
-    private void refreshPages(Range pages) {
-        for (int page : pages) {
-            PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
-                    page,
-                    sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                    mPaginationModel.getPageSize(page),
-                    new PageTouchHandler());
-            pageView.requestDrawAtZoom(mStableZoom);
-            loadVisiblePageText(page);
-            maybeLoadFormAccessibilityInfo(page);
-        }
-    }
-
-    private void refreshPagesAfterLayout(final Range pages) {
-        ThreadUtils.postOnUiThread(
-                () -> {
-                    if (viewState().get() != ViewState.NO_VIEW) {
-                        refreshPages(pages);
-                    }
-                });
-    }
-
-    private void clearPages(Range pages, boolean clearViews) {
-        for (int page : pages) {
-            // Don't cancel search - search results for the current search are always useful,
-            // even for pages we can't see right now. Form filling operations should always
-            // be executed against the document, even if the user has scrolled away from the page.
-            mPdfLoader.cancelExceptSearchAndFormFilling(page);
-            if (clearViews) {
-                mPaginatedView.removeViewAt(page);
-            }
-        }
+        mLayoutHandler.maybeLayoutPages(
+                mPaginatedView.getPageRangeHandler().getVisiblePages().getLast());
     }
 
     /** Show the loading spinner. */
@@ -935,197 +677,13 @@
         }
     }
 
-    private void loadVisiblePageText(int page) {
-        PageView pageView = mPageViewFactory.getOrCreatePageView(
-                page,
-                sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                mPaginationModel.getPageSize(page),
-                new PageTouchHandler());
-        PageMosaicView pageMosaicView = pageView.getPageView();
-        if (pageMosaicView.needsPageText()) {
-            mPdfLoader.loadPageText(page);
-        }
-        if (!pageMosaicView.hasPageUrlLinks()) {
-            mPdfLoader.loadPageUrlLinks(page);
-        }
-        if (!pageMosaicView.hasPageGotoLinks()) {
-            mPdfLoader.loadPageGotoLinks(page);
-        }
-        if (page == mSelectionModel.getPage()) {
-            pageMosaicView.setOverlay(new PdfHighlightOverlay(mSelectionModel.selection().get()));
-        } else if (mSearchModel.query().get() != null) {
-            if (!pageMosaicView.hasOverlay()) {
-                mPdfLoader.searchPageText(page, mSearchModel.query().get());
-            }
-        } else {
-            pageMosaicView.setOverlay(null);
-        }
-    }
-
-    /**
-     * Load accessibility information for the form if document can be edited and accessibility is
-     * required.
-     */
-    private void maybeLoadFormAccessibilityInfo(int pageNum) {
-        mPageViewFactory.getOrCreatePageView(
-                pageNum,
-                sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                mPaginationModel.getPageSize(pageNum),
-                new PageTouchHandler());
-    }
-
-    /** Computes the range of visible pages in the given position. */
-    private Range computeVisibleRange(ZoomScroll position) {
-        int top = Math.round(position.scrollY / position.zoom);
-        int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
-        Range window = new Range(top, bottom);
-        return mPaginationModel.getPagesInWindow(window, true);
-    }
-
-    /** Expand the range to include more page(s) in the each direction. */
-    private static Range expandRange(Range range, int margin, Range allPages) {
-        return range.expand(margin, allPages);
-    }
-
-    /**
-     * Computes the range of pages that are entirely visible, or if no page is entirely visible,
-     * returns the most visible page.
-     */
-    private Range computeImportantRange(ZoomScroll position) {
-        int top = Math.round(position.scrollY / position.zoom);
-        int bottom = Math.round((position.scrollY + mZoomView.getHeight()) / position.zoom);
-        Range window = new Range(top, bottom);
-        return mPaginationModel.getPagesInWindow(window, false);
-    }
-
-    { // Listen to ZoomView.
-        mZoomScrollObserver =
-                new ValueObserver<ZoomScroll>() {
-                    @Override
-                    public void onChange(ZoomScroll oldPosition, ZoomScroll position) {
-                        loadPageAssets(position);
-                        if (mPageIndicator.setRangeAndZoom(
-                                computeImportantRange(position), position.zoom, position.stable)) {
-                            showFastScrollView();
-                        }
-
-                        if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
-                            if (position.scrollY > 0) {
-                                mAnnotationButton.setVisibility(View.GONE);
-                            } else if (position.scrollY == 0
-                                    && mAnnotationButton.getVisibility() == View.GONE
-                                    && mFindInFileView.getVisibility() == View.GONE) {
-                                mAnnotationButton.setVisibility(View.VISIBLE);
-                            }
-                        }
-                    }
-
-                    @NonNull
-                    @Override
-                    public String toString() {
-                        return TAG + "#zoomScrollObserver";
-                    }
-                };
-    }
-
-    { // Listen to searchModel.
-        mSearchQueryObserver =
-                new ValueObserver<String>() {
-                    @Override
-                    public void onChange(String oldQuery, String newQuery) {
-                        mPaginatedView.clearAllOverlays();
-                    }
-
-                    @NonNull
-                    @Override
-                    public String toString() {
-                        return TAG + "#searchQueryObserver";
-                    }
-                };
-
-        mSelectedMatchObserver =
-                new ValueObserver<SelectedMatch>() {
-                    @Override
-                    public void onChange(
-                            @Nullable SelectedMatch oldSelection,
-                            @Nullable SelectedMatch newSelection) {
-                        if (newSelection == null) {
-                            mPaginatedView.clearAllOverlays();
-                            return;
-                        }
-                        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
-                            // Selected match has moved onto a new page - update the overlay on
-                            // the old page.
-                            getPage(oldSelection.getPage())
-                                    .getPageView()
-                                    .setOverlay(
-                                            new PdfHighlightOverlay(oldSelection.getPageMatches()));
-                        }
-                        lookAtSelection(newSelection);
-                    }
-
-                    @NonNull
-                    @Override
-                    public String toString() {
-                        return TAG + "#selectedMatchObserver";
-                    }
-                };
-    }
-
-    { // Listen to selectionModel.
-        mSelectionObserver =
-                new ValueObserver<PageSelection>() {
-                    @Override
-                    public void onChange(PageSelection oldSelection, PageSelection newSelection) {
-                        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
-                            getPage(oldSelection.getPage()).getPageView().setOverlay(null);
-                        }
-                        if (newSelection != null && mVisiblePages.contains(
-                                newSelection.getPage())) {
-                            ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
-                                    newSelection.getPage(),
-                                    sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                                    mPaginationModel.getPageSize(newSelection.getPage()),
-                                    new PageTouchHandler()))
-                                    .setOverlay(new PdfHighlightOverlay(newSelection));
-                        }
-                    }
-
-                    @NonNull
-                    @Override
-                    public String toString() {
-                        return TAG + "#selectionObserver";
-                    }
-                };
-    }
-
-    {
-        mFastscrollerPositionObserver =
-                new ValueObserver<Integer>() {
-                    @Override
-                    public void onChange(@Nullable Integer oldValue, @Nullable Integer newValue) {
-                        if (mPageIndicator != null && newValue != null) {
-                            mPageIndicator.getView().setY(
-                                    newValue - (mPageIndicator.getView().getHeight() / 2));
-                            mPageIndicator.show();
-                            showFastScrollView();
-                        }
-                    }
-
-                    @NonNull
-                    @Override
-                    public String toString() {
-                        return TAG + "#fastscrollerPositionObserver";
-                    }
-                };
-    }
-
     private FindInFileListener makeFindInFileListener() {
         return new FindInFileListener() {
             @Override
             public boolean onQueryTextChange(@Nullable String query) {
                 if (mSearchModel != null) {
-                    mSearchModel.setQuery(query, getViewingPage());
+                    mSearchModel.setQuery(query,
+                            mPaginatedView.getPageRangeHandler().getVisiblePage());
                     return true;
                 }
                 return false;
@@ -1142,7 +700,8 @@
                         direction = CycleRange.Direction.FORWARDS;
                         // TODO: Track "find next" action event.
                     }
-                    mSearchModel.selectNextMatch(direction, getViewingPage());
+                    mSearchModel.selectNextMatch(direction,
+                            mPaginatedView.getPageRangeHandler().getVisiblePage());
                     return true;
                 }
                 return false;
@@ -1156,113 +715,6 @@
         };
     }
 
-    public class PageTouchHandler {
-        /**
-         * Handles a tap event for non-formfilling actions.
-         *
-         * <p>This is includes comments, links and full screen toggle. Separate from form filling as
-         * form filling involves asynchronous evaluations that must be completed outside normal
-         * branch
-         * statements.
-         */
-        public boolean handleSingleTapNoFormFilling(@NonNull MotionEvent event,
-                @NonNull PageMosaicView pageMosaicView) {
-            if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
-                if (mAnnotationButton.getVisibility() == View.GONE
-                        && mFindInFileView.getVisibility() == GONE) {
-                    mAnnotationButton.setVisibility(View.VISIBLE);
-                } else {
-                    mAnnotationButton.setVisibility(View.GONE);
-                }
-            }
-            boolean hadSelection =
-                    mSelectionModel != null && mSelectionModel.selection().get() != null;
-            if (hadSelection) {
-                mSelectionModel.setSelection(null);
-            }
-
-            Point point = new Point((int) event.getX(), (int) event.getY());
-            String linkUrl = pageMosaicView.getLinkUrl(point);
-            if (linkUrl != null) {
-                ExternalLinks.open(linkUrl, requireActivity());
-            }
-
-            GotoLinkDestination gotoDest = pageMosaicView.getGotoDestination(point);
-            if (gotoDest != null) {
-                gotoPageDest(gotoDest);
-            }
-
-            return true;
-        }
-
-        /** */
-        public void gotoPageDest(@NonNull GotoLinkDestination destination) {
-
-            if (destination.getPageNumber() >= mPaginationModel.getSize()) {
-                // We have not yet loaded our destination.
-                layoutPages(destination.getPageNumber() + 1);
-                mDimensCallbackQueue.add(
-                        pageNum -> {
-                            if (pageNum == destination.getPageNumber()) {
-                                gotoPageDest(destination);
-                                return false;
-                            }
-                            return true;
-                        });
-                return;
-            }
-
-            if (destination.getYCoordinate() != null) {
-                int pageY = (int) destination.getYCoordinate().floatValue();
-
-                Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber());
-                int x = pageRect.left + (pageRect.width() / 2);
-                int y = mPaginationModel.getLookAtY(destination.getPageNumber(), pageY);
-                // Zoom should match the width of the page.
-                float zoom =
-                        ZoomUtils.calculateZoomToFit(
-                                mZoomView.getViewportWidth(), mZoomView.getViewportHeight(),
-                                pageRect.width(), 1);
-
-                mZoomView.setZoom(zoom);
-                mZoomView.centerAt(x, y);
-            } else {
-                gotoPage(destination.getPageNumber());
-            }
-        }
-
-        /** Goes to the {@code pageNum} and fits the page to the current viewport. */
-        private void gotoPage(int pageNum) {
-            if (pageNum >= mPaginationModel.getSize()) {
-                // We have not yet loaded our destination.
-                layoutPages(pageNum + 1);
-                mDimensCallbackQueue.add(
-                        loadedPageNum -> {
-                            if (pageNum == loadedPageNum) {
-                                gotoPage(pageNum);
-                                return false;
-                            }
-                            return true;
-                        });
-                return;
-            }
-
-            Rect pageRect = mPaginationModel.getPageLocation(pageNum);
-
-            int x = pageRect.left + (pageRect.width() / 2);
-            int y = pageRect.top + (pageRect.height() / 2);
-            float zoom =
-                    ZoomUtils.calculateZoomToFit(
-                            mZoomView.getViewportWidth(),
-                            mZoomView.getViewportHeight(),
-                            pageRect.width(),
-                            pageRect.height());
-
-            mZoomView.setZoom(zoom);
-            mZoomView.centerAt(x, y);
-        }
-    }
-
     // TODO: Revisit this method for its usage. Currently redundant
 
     { // Init pdfLoaderCallbacks
@@ -1329,19 +781,17 @@
                         }
 
                         mDocumentLoaded = true;
-                        PdfViewer.this.mNumPages = numPages;
-
                         hideSpinner();
 
                         // Assume we see at least the first page
-                        mMaxPage = 1;
+                        mPaginatedView.getPageRangeHandler().setMaxPage(1);
                         if (viewState().get() != ViewState.NO_VIEW) {
                             mPaginationModel.initialize(numPages);
                             mPaginatedView.setModel(mPaginationModel);
                             mPaginationModel.addObserver(mPaginatedView);
 
                             dismissPasswordDialog();
-                            maybeLayoutPages(1);
+                            mLayoutHandler.maybeLayoutPages(1);
                             mPageIndicator.setNumPages(numPages);
                             mSearchModel.setNumPages(numPages);
                         }
@@ -1350,7 +800,7 @@
                             mShouldRedrawOnDocumentLoaded = false;
                         }
 
-                        if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
+                        if (mIsAnnotationIntentResolvable) {
                             mAnnotationButton.setVisibility(VISIBLE);
                         }
                     }
@@ -1392,8 +842,7 @@
                             ((PageMosaicView) mPageViewFactory.getOrCreatePageView(
                                     page,
                                     sScreen.pxFromDp(PAGE_ELEVATION_DP),
-                                    mPaginationModel.getPageSize(page),
-                                    new PageTouchHandler()))
+                                    mPaginationModel.getPageSize(page)))
                                     .setFailure(getString(R.string.error_on_page, page + 1));
                             Toaster.LONG.popToast(getActivity(), R.string.error_on_page, page + 1);
                             // TODO: Track render error.
@@ -1413,7 +862,7 @@
                     public void setPageDimensions(int pageNum, @NonNull Dimensions dimensions) {
                         if (viewState().get() != ViewState.NO_VIEW) {
                             mPaginationModel.addPage(pageNum, dimensions);
-                            mPageLayoutReach = mPaginationModel.getSize();
+                            mLayoutHandler.setPageLayoutReach(mPaginationModel.getSize());
 
                             if (mSearchModel.query().get() != null
                                     && mSearchModel.selectedMatch().get() != null
@@ -1427,35 +876,24 @@
                                         });
                             }
 
-                            ThreadUtils.postOnUiThread(
-                                    () -> {
-                                        if (mDimensCallbackQueue.isEmpty()
-                                                || viewState().get() == ViewState.NO_VIEW) {
-                                            return;
-                                        }
-
-                                        Iterator<OnDimensCallback> iterator =
-                                                mDimensCallbackQueue.iterator();
-                                        while (iterator.hasNext()) {
-                                            OnDimensCallback callback = iterator.next();
-                                            boolean shouldKeep = callback.onDimensLoaded(pageNum);
-                                            if (!shouldKeep) {
-                                                iterator.remove();
-                                            }
-                                        }
-                                    });
+                            mLayoutHandler.processCallbacksInQueue(viewState().get(), pageNum);
 
                             // The new page might actually be visible on the screen, so we need
                             // to fetch assets:
-                            Range newRange = computeVisibleRange(mZoomView.zoomScroll().get());
+                            ZoomScroll position = mZoomView.zoomScroll().get();
+                            Range newRange =
+                                    mPaginatedView.getPageRangeHandler().computeVisibleRange(
+                                            position.scrollY, position.zoom, mZoomView.getHeight(),
+                                            true);
                             if (newRange.isEmpty()) {
                                 // During fast-scroll, we mostly don't need to fetch assets, but
                                 // make sure we keep pushing layout bounds far enough, and update
                                 // page numbers as we "scroll" down.
-                                if (mPageIndicator.setRangeAndZoom(newRange, mStableZoom, false)) {
+                                if (mPageIndicator.setRangeAndZoom(newRange,
+                                        mZoomView.getStableZoom(), false)) {
                                     showFastScrollView();
                                 }
-                                maybeLayoutPages(newRange.getLast());
+                                mLayoutHandler.maybeLayoutPages(newRange.getLast());
                             } else if (newRange.contains(pageNum)) {
                                 // The new page is visible, fetch its assets.
                                 loadPageAssets(mZoomView.zoomScroll().get());
@@ -1512,10 +950,6 @@
                             return;
                         }
                         if (selection != null) {
-                            if (mWaitingOnSelectionToCreateInlineComment) {
-                                mSelectionModel.setSelection(selection);
-                                return;
-                            }
                             // Clear searchModel - we hide the search and show the selection
                             // instead.
                             mSearchModel.setQuery(null, -1);
@@ -1665,7 +1099,7 @@
             queryBox.clearFocus();
             queryBox.setText("");
             parentLayout.setVisibility(GONE);
-            if (AnnotationUtils.launchAnnotationIntent(requireContext(), mLocalUri)) {
+            if (mIsAnnotationIntentResolvable) {
                 mAnnotationButton.setVisibility(VISIBLE);
             }
         });
@@ -1687,4 +1121,5 @@
         intent.setData(mLocalUri);
         startActivity(intent);
     }
+
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
similarity index 60%
copy from pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
copy to pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
index 5138d2d..081e10d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SearchQueryObserver.java
@@ -14,27 +14,21 @@
  * limitations under the License.
  */
 
-package androidx.pdf.pdflib;
+package androidx.pdf.viewer;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.pdf.models.PdfDocumentRemote;
+import androidx.pdf.util.ObservableValue;
 
-/**
- *
- */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public class PdfDocumentRemoteProto {
-    private PdfDocumentRemote mRemote;
+class SearchQueryObserver implements ObservableValue.ValueObserver<String> {
+    private final PaginatedView mPaginatedView;
 
-    public PdfDocumentRemoteProto(@NonNull PdfDocumentRemote remote) {
-        this.mRemote = remote;
+    SearchQueryObserver(PaginatedView paginatedView) {
+        mPaginatedView = paginatedView;
     }
 
-    @NonNull
-    public PdfDocumentRemote getPdfDocumentRemote() {
-        return mRemote;
+    @Override
+    public void onChange(String oldQuery, String newQuery) {
+        mPaginatedView.clearAllOverlays();
     }
-
-    // TODO: Add goto links methods from the original kotlin file
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
new file mode 100644
index 0000000..734a15e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SelectedMatchValueObserver.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.PaginationUtils;
+import androidx.pdf.widget.ZoomView;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SelectedMatchValueObserver implements ObservableValue.ValueObserver<SelectedMatch> {
+    private final PaginatedView mPaginatedView;
+    private final PaginationModel mPaginationModel;
+    private final PageViewFactory mPageViewFactory;
+    private final LayoutHandler mLayoutHandler;
+    private final ZoomView mZoomView;
+    private final Context mContext;
+
+    SelectedMatchValueObserver(PaginatedView paginatedView, PaginationModel paginationModel,
+            PageViewFactory pageViewFactory, ZoomView zoomView, LayoutHandler layoutHandler,
+            Context context) {
+        mPaginatedView = paginatedView;
+        mPaginationModel = paginationModel;
+        mPageViewFactory = pageViewFactory;
+        mZoomView = zoomView;
+        mLayoutHandler = layoutHandler;
+        mContext = context;
+    }
+
+    @Override
+    public void onChange(
+            @Nullable SelectedMatch oldSelection,
+            @Nullable SelectedMatch newSelection) {
+        if (newSelection == null) {
+            mPaginatedView.clearAllOverlays();
+            return;
+        }
+        if (oldSelection != null && isPageCreated(oldSelection.getPage())) {
+            // Selected match has moved onto a new page - update the overlay on
+            // the old page.
+            mPaginatedView.getViewAt(oldSelection.getPage())
+                    .getPageView()
+                    .setOverlay(
+                            new PdfHighlightOverlay(oldSelection.getPageMatches()));
+        }
+        lookAtSelection(newSelection);
+    }
+
+    private boolean isPageCreated(int pageNum) {
+        return pageNum < mPaginationModel.getSize() && mPaginatedView.getViewAt(pageNum) != null;
+    }
+
+    private void lookAtSelection(SelectedMatch selection) {
+        if (selection == null || selection.isEmpty()) {
+            return;
+        }
+        if (selection.getPage() >= mPaginationModel.getSize()) {
+            mLayoutHandler.layoutPages(selection.getPage() + 1);
+            return;
+        }
+        Rect rect = selection.getPageMatches().getFirstRect(selection.getSelected());
+        int x = mPaginationModel.getLookAtX(selection.getPage(), rect.centerX());
+        int y = mPaginationModel.getLookAtY(selection.getPage(), rect.centerY());
+        mZoomView.centerAt(x, y);
+
+        PageMosaicView pageView = (PageMosaicView) mPageViewFactory.getOrCreatePageView(
+                selection.getPage(),
+                PaginationUtils.getPageElevationInPixels(mContext),
+                mPaginationModel.getPageSize(selection.getPage()));
+        pageView.setOverlay(selection.getOverlay());
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
new file mode 100644
index 0000000..74f02a6
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static android.view.View.GONE;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.models.GotoLinkDestination;
+import androidx.pdf.util.ExternalLinks;
+import androidx.pdf.util.ZoomUtils;
+import androidx.pdf.widget.ZoomView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SingleTapHandler {
+    private final Context mContext;
+    private final FloatingActionButton mFloatingActionButton;
+    private final FindInFileView mFindInFileView;
+    private final ZoomView mZoomView;
+    private final PdfSelectionModel mPdfSelectionModel;
+    private final PaginationModel mPaginationModel;
+    private final LayoutHandler mLayoutHandler;
+    private boolean mIsAnnotationIntentResolvable;
+
+    public SingleTapHandler(@NonNull Context context,
+            @NonNull FloatingActionButton floatingActionButton,
+            @NonNull FindInFileView findInFileView,
+            @NonNull ZoomView zoomView,
+            @NonNull PdfSelectionModel pdfSelectionModel,
+            @NonNull PaginationModel paginationModel,
+            @NonNull LayoutHandler layoutHandler) {
+        mContext = context;
+        mFloatingActionButton = floatingActionButton;
+        mFindInFileView = findInFileView;
+        mZoomView = zoomView;
+        mPdfSelectionModel = pdfSelectionModel;
+        mPaginationModel = paginationModel;
+        mLayoutHandler = layoutHandler;
+    }
+
+    public void setAnnotationIntentResolvable(boolean annotationIntentResolvable) {
+        mIsAnnotationIntentResolvable = annotationIntentResolvable;
+    }
+
+    /** */
+    public void handleSingleTapConfirmedEventOnPage(@NonNull MotionEvent event,
+            @NonNull PageMosaicView pageMosaicView) {
+        if (mIsAnnotationIntentResolvable) {
+            if (mFloatingActionButton.getVisibility() == View.GONE
+                    && mFindInFileView.getVisibility() == GONE) {
+                mFloatingActionButton.setVisibility(View.VISIBLE);
+            } else {
+                mFloatingActionButton.setVisibility(View.GONE);
+            }
+        }
+
+        handleSelection();
+
+        Point point = new Point((int) event.getX(), (int) event.getY());
+        handleExternalLink(point, pageMosaicView);
+
+        GotoLinkDestination gotoDest = pageMosaicView.getGotoDestination(point);
+        if (gotoDest != null) {
+            gotoPageDest(gotoDest);
+        }
+    }
+
+    private void handleSelection() {
+        boolean hadSelection =
+                mPdfSelectionModel != null && mPdfSelectionModel.selection().get() != null;
+        if (hadSelection) {
+            mPdfSelectionModel.setSelection(null);
+        }
+    }
+
+    private void handleExternalLink(Point point, PageMosaicView pageMosaicView) {
+        String linkUrl = pageMosaicView.getLinkUrl(point);
+        if (linkUrl != null) {
+            ExternalLinks.open(linkUrl, mContext);
+        }
+    }
+
+    /**  */
+    public void gotoPageDest(@NonNull GotoLinkDestination destination) {
+
+        if (destination.getPageNumber() >= mPaginationModel.getSize()) {
+            // We have not yet loaded our destination.
+            mLayoutHandler.layoutPages(destination.getPageNumber() + 1);
+            mLayoutHandler.add(
+                    pageNum -> {
+                        if (pageNum == destination.getPageNumber()) {
+                            gotoPageDest(destination);
+                            return false;
+                        }
+                        return true;
+                    });
+            return;
+        }
+
+        if (destination.getYCoordinate() != null) {
+            int pageY = (int) destination.getYCoordinate().floatValue();
+
+            Rect pageRect = mPaginationModel.getPageLocation(destination.getPageNumber());
+            int x = pageRect.left + (pageRect.width() / 2);
+            int y = mPaginationModel.getLookAtY(destination.getPageNumber(), pageY);
+            // Zoom should match the width of the page.
+            float zoom =
+                    ZoomUtils.calculateZoomToFit(
+                            mZoomView.getViewportWidth(), mZoomView.getViewportHeight(),
+                            pageRect.width(), 1);
+
+            mZoomView.setZoom(zoom);
+            mZoomView.centerAt(x, y);
+        } else {
+            gotoPage(destination.getPageNumber());
+        }
+    }
+
+    /** Goes to the {@code pageNum} and fits the page to the current viewport. */
+    private void gotoPage(int pageNum) {
+        if (pageNum >= mPaginationModel.getSize()) {
+            // We have not yet loaded our destination.
+            mLayoutHandler.layoutPages(pageNum + 1);
+            mLayoutHandler.add(
+                    loadedPageNum -> {
+                        if (pageNum == loadedPageNum) {
+                            gotoPage(pageNum);
+                            return false;
+                        }
+                        return true;
+                    });
+            return;
+        }
+
+        Rect pageRect = mPaginationModel.getPageLocation(pageNum);
+
+        int x = pageRect.left + (pageRect.width() / 2);
+        int y = pageRect.top + (pageRect.height() / 2);
+        float zoom =
+                ZoomUtils.calculateZoomToFit(
+                        mZoomView.getViewportWidth(),
+                        mZoomView.getViewportHeight(),
+                        pageRect.width(),
+                        pageRect.height());
+
+        mZoomView.setZoom(zoom);
+        mZoomView.centerAt(x, y);
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
index 64a949d..b81b180 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/Viewer.java
@@ -56,14 +56,8 @@
 @SuppressWarnings("deprecation")
 public abstract class Viewer extends Fragment {
 
-    @NonNull
-    protected abstract String getLogTag();
-
     protected static final String KEY_DATA = "data";
 
-    /** Scale for the progress metric. */
-    protected static final int PROGRESS_SCALER = 100;
-
     /**
      * The state of the view hierarchy for this {@link Fragment}, as exposed by {@link #mViewState}.
      */
@@ -121,9 +115,6 @@
     protected ExposedValue<ViewState> mViewState = Observables.newExposedValueWithInitialValue(
             ViewState.NO_VIEW);
 
-    // Debug log of lifecycle events that happened on this viewer, helps investigating.
-    private final StringBuilder mEventlog = new StringBuilder();
-
     {
         // We can call getArguments() from setters and know that it will not be null.
         setArguments(new Bundle());
@@ -203,13 +194,6 @@
         }
     }
 
-    /** Notifies this Viewer goes off-screen. {@link #onExit()} will be called immediately. */
-    public void exit() {
-        mDelayedEnter = false; // in case we never started.
-        onExit();
-        mOnScreen = false;
-    }
-
     /** Called after this viewer enters the screen and becomes visible. */
     @CallSuper
     protected void onEnter() {
@@ -314,18 +298,6 @@
         getArguments().putBundle(KEY_DATA, data.asBundle());
     }
 
-    /** Returns a compact event log for this Viewer that helps investigating lifecycle issues. */
-    @NonNull
-    protected String getEventlog() {
-        return mEventlog.toString();
-    }
-
-    /** Returns the length of the current file. The meaning of the length is type dependent. */
-    public abstract long getContentLength();
-
-    /** Returns the user's current progress in the file in percentage. */
-    public abstract int getViewProgress();
-
     @Override
     protected void finalize() throws Throwable {
         super.finalize();
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
new file mode 100644
index 0000000..90bb371
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import android.view.View;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.widget.FastScrollView;
+import androidx.pdf.widget.ZoomView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+class ZoomScrollValueObserver implements ObservableValue.ValueObserver<ZoomView.ZoomScroll> {
+    private final PaginatedView mPaginatedView;
+    private final ZoomView mZoomView;
+    private final LayoutHandler mLayoutHandler;
+    private final FloatingActionButton mAnnotationButton;
+    private final FindInFileView mFindInFileView;
+    private final PageIndicator mPageIndicator;
+    private final FastScrollView mFastScrollView;
+    private final boolean mIsAnnotationIntentResolvable;
+    private final ObservableValue<Viewer.ViewState> mViewState;
+
+    ZoomScrollValueObserver(ZoomView zoomView, PaginatedView paginatedView,
+            LayoutHandler layoutHandler,
+            FloatingActionButton annotationButton, FindInFileView findInFileView,
+            PageIndicator pageIndicator, FastScrollView fastScrollView,
+            boolean isAnnotationIntentResolvable, ObservableValue<Viewer.ViewState> viewState) {
+        mZoomView = zoomView;
+        mPaginatedView = paginatedView;
+        mLayoutHandler = layoutHandler;
+        mAnnotationButton = annotationButton;
+        mFindInFileView = findInFileView;
+        mPageIndicator = pageIndicator;
+        mFastScrollView = fastScrollView;
+        mIsAnnotationIntentResolvable = isAnnotationIntentResolvable;
+        mViewState = viewState;
+    }
+
+    @Override
+    public void onChange(ZoomView.ZoomScroll oldPosition, ZoomView.ZoomScroll position) {
+        loadPageAssets(position);
+        Range visiblePageRange = mPaginatedView.getPageRangeHandler()
+                .computeVisibleRange(position.scrollY, position.zoom,
+                        mZoomView.getHeight(), false);
+        if (mPageIndicator.setRangeAndZoom(visiblePageRange, position.zoom,
+                position.stable)) {
+            showFastScrollView();
+        }
+
+        if (mIsAnnotationIntentResolvable) {
+            if (position.scrollY > 0) {
+                mAnnotationButton.setVisibility(View.GONE);
+            } else if (position.scrollY == 0
+                    && mAnnotationButton.getVisibility() == View.GONE
+                    && mFindInFileView.getVisibility() == View.GONE) {
+                mAnnotationButton.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    private void loadPageAssets(ZoomView.ZoomScroll position) {
+        // Change the resolution of the bitmaps only when a gesture is not in progress.
+        if (position.stable || mZoomView.getStableZoom() == 0) {
+            mZoomView.setStableZoom(position.zoom);
+        }
+
+        mPaginatedView.getPaginationModel().setViewArea(mZoomView.getVisibleAreaInContentCoords());
+        mPaginatedView.refreshPageRangeInVisibleArea(position, mZoomView.getHeight());
+        mPaginatedView.handleGonePages(/* clearViews= */ false);
+        mPaginatedView.loadInvisibleNearPageRange(mZoomView.getStableZoom());
+
+        // The step (4) below requires page Views to be created and laid out. So we create them here
+        // and set this flag if that operation needs to wait for a layout pass.
+        boolean requiresLayoutPass = mPaginatedView.createPageViewsForVisiblePageRange();
+
+        // 4. Refresh tiles and/or full pages.
+        if (position.stable) {
+            // Perform a full refresh on all visible pages
+            mPaginatedView.refreshVisiblePages(requiresLayoutPass, mViewState.get(),
+                    mZoomView.getStableZoom());
+            mPaginatedView.handleGonePages(/* clearViews= */ true);
+        } else if (mZoomView.getStableZoom() == position.zoom) {
+            // Just load a few more tiles in case of tile-scroll
+            mPaginatedView.refreshVisibleTiles(requiresLayoutPass, mViewState.get());
+        }
+
+        if (mPaginatedView.getPageRangeHandler().getVisiblePages() != null) {
+            mLayoutHandler.maybeLayoutPages(
+                    mPaginatedView.getPageRangeHandler().getVisiblePages().getLast());
+        }
+    }
+
+    private void showFastScrollView() {
+        if (mFastScrollView != null) {
+            mFastScrollView.setVisible();
+        }
+    }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
index fafea44..7da2a94 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractPdfTask.java
@@ -21,7 +21,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
 import androidx.pdf.util.ThreadUtils;
 
 /**
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
index ce69c0cb..7c1e5d7 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/AbstractWriteTask.java
@@ -20,7 +20,7 @@
 import android.os.RemoteException;
 
 import androidx.annotation.RestrictTo;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
 
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
index 5758c6a..df72681 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
@@ -26,7 +26,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.pdf.models.PdfDocumentRemote;
-import androidx.pdf.pdflib.PdfDocumentService;
+import androidx.pdf.service.PdfDocumentService;
 import androidx.pdf.util.Preconditions;
 
 import java.util.concurrent.locks.Condition;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
index 2884cd6..933f7a8 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
@@ -32,7 +32,7 @@
 import androidx.pdf.models.Dimensions;
 import androidx.pdf.models.PdfDocumentRemote;
 import androidx.pdf.models.SelectionBoundary;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
+import androidx.pdf.service.PdfDocumentRemoteProto;
 import androidx.pdf.util.BitmapRecycler;
 import androidx.pdf.util.TileBoard.TileInfo;
 
@@ -67,6 +67,7 @@
 
     private final SparseArray<PdfPageLoader> mPageLoaders;
     private String mLoadedPassword;
+    private int mNumPages;
 
     /** Creates a new {@link PdfLoader} and loads the document from the given data. */
     @NonNull
@@ -126,6 +127,14 @@
         this.mPageLoaders = new SparseArray<>();
     }
 
+    public int getNumPages() {
+        return mNumPages;
+    }
+
+    public void setNumPages(int numPages) {
+        mNumPages = numPages;
+    }
+
     /** Schedule task to load a PdfDocument. */
     public void reloadDocument() {
         mExecutor.schedule(new LoadDocumentTask(mLoadedPassword));
@@ -347,7 +356,6 @@
     /** AsyncTask for loading a PdfDocument. */
     class LoadDocumentTask extends AbstractPdfTask<PdfStatus> {
         private final String mPassword;
-        private int mNumPages;
 //        private boolean mIsLinearized;
 
         LoadDocumentTask() {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
index 61cddc6..d446bf0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java
@@ -30,9 +30,7 @@
 import androidx.pdf.models.MatchRects;
 import androidx.pdf.models.PageSelection;
 import androidx.pdf.models.SelectionBoundary;
-import androidx.pdf.pdflib.PdfDocumentRemoteProto;
-import androidx.pdf.util.BitmapParcel;
-import androidx.pdf.util.StrictModeUtils;
+import androidx.pdf.service.PdfDocumentRemoteProto;
 import androidx.pdf.util.TileBoard.TileInfo;
 
 import com.google.common.collect.ImmutableList;
@@ -58,14 +56,6 @@
     /** Arbitrary dimensions used for pages that are broken. */
     private static final Dimensions DEFAULT_PAGE = new Dimensions(400, 400);
 
-    static {
-        // TODO: StrictMode- disk read 14ms.
-        // NOTE: this line can break when running with --noforge, such as when debugging with
-        // Android Studio. You may need to comment it out locally if you see errors like
-        // `java.lang.UnsatisfiedLinkError: no bitmap_parcel in java.library.path`.
-        StrictModeUtils.bypass(() -> BitmapParcel.loadNdkLib());
-    }
-
     private final PdfLoader mParent;
     private final int mPageNum;
     private final boolean mHideTextAnnotations;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
index faa86b3..178d2b6 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ZoomView.java
@@ -54,6 +54,8 @@
 import androidx.pdf.util.Screen;
 import androidx.pdf.util.ThreadUtils;
 import androidx.pdf.util.ZoomScrollRestorer;
+import androidx.pdf.util.ZoomUtils;
+import androidx.pdf.viewer.PdfSelectionModel;
 
 import com.google.android.material.motion.MotionUtils;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -196,6 +198,9 @@
     private int mContentResizedModeZoom = ContentResizedMode.KEEP_SAME_ABSOLUTE;
     private boolean mOverrideMinZoomToFit = false;
     private boolean mOverrideMaxZoomToFit = false;
+
+    /** The last stable zoom: we only re-draw bitmaps at stable zoom (not during a gesture). */
+    private float mStableZoom;
     /**
      * If set to true, suppresses {@link ZoomGestureHandler#onScale(ScaleGestureDetector)} behavior.
      */
@@ -211,6 +216,11 @@
      */
     private Rect mPaddingOnLastViewportUpdate;
 
+    /** Base padding for ZoomView in px as set in saveZoomViewBasePadding(). */
+    private Rect mZoomViewBasePadding = new Rect();
+    private boolean mZoomViewBasePaddingSaved;
+    private PdfSelectionModel mPdfSelectionModel;
+
     {
         mScroller = new RelativeScroller(getContext());
         mPosition = Observables.newExposedValueWithInitialValue(new ZoomScroll(1, 0, 0, STABLE));
@@ -234,51 +244,6 @@
         ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR);
     }
 
-    private static int scrollDeltaNeededForZoomChange(
-            float oldZoom, float newZoom, float zoomviewPivot, int scroll) {
-        // Find where the given pivot point would move to when we change the zoom, and return the
-        // delta.
-        float contentPivot = toContentCoord(zoomviewPivot, oldZoom, scroll);
-        float movedZoomViewPivot = toZoomViewCoord(contentPivot, newZoom, scroll);
-        return (int) (movedZoomViewPivot - zoomviewPivot);
-    }
-
-    private static float toContentCoord(float zoomViewCoord, float zoom, int scroll) {
-        return (zoomViewCoord + scroll) / zoom;
-    }
-
-    private static float toZoomViewCoord(float contentCoord, float zoom, int scroll) {
-        return (contentCoord * zoom) - scroll;
-    }
-
-    private static int constrain(float zoom, int scroll, int contentRawSize, int viewportSize) {
-        // The variables in this method are named left and right, which is accurate when this
-        // method is used to constrain X position - when constraining Y, left means top and right
-        // means bottom.
-
-        // Find the left and right bounds of the content in the zoomview's co-ordinates.
-        float leftBound = toZoomViewCoord(0, zoom, scroll);
-        float rightBound = toZoomViewCoord(contentRawSize, zoom, scroll);
-
-        if (leftBound <= 0 && rightBound >= viewportSize) {
-            // Content too large for viewport and no dead margins: no adjustment needed.
-            return 0;
-        }
-        float scaledContentSize = rightBound - leftBound;
-        if (scaledContentSize <= viewportSize) {
-            // Content fits in viewport: keep in the center.
-            return (int) ((rightBound + leftBound - viewportSize) / 2);
-        } else {
-            // Content doesn't fit in viewport: eliminate dead margins.
-            if (leftBound > 0) { // Dead margin on the left.
-                return (int) leftBound;
-            } else if (rightBound < viewportSize) { // Dead margin on the right.
-                return (int) (rightBound - viewportSize);
-            }
-        }
-        return 0;
-    }
-
     /**
      * Set values of shareScrollToLeft, shareScrollToRight, shareScrollToTop and
      * shareScrollToBottom.
@@ -412,6 +377,15 @@
         return this;
     }
 
+    @Nullable
+    public PdfSelectionModel getPdfSelectionModel() {
+        return mPdfSelectionModel;
+    }
+
+    public void setPdfSelectionModel(@NonNull PdfSelectionModel pdfSelectionModel) {
+        mPdfSelectionModel = pdfSelectionModel;
+    }
+
     /** Exposes this view's position as an observable value. */
     @NonNull
     public ObservableValue<ZoomScroll> zoomScroll() {
@@ -658,6 +632,13 @@
         this.mMaxZoom = maxZoom;
     }
 
+    public float getStableZoom() {
+        return mStableZoom;
+    }
+
+    public void setStableZoom(float stableZoom) {
+        this.mStableZoom = stableZoom;
+    }
     private float getConstrainedZoomToFit() {
         return constrainZoom(getUnconstrainedZoomToFit());
     }
@@ -764,8 +745,10 @@
         }
         float left = x * newZoom - mViewport.width() / 2f;
         float top = y * newZoom - mViewport.height() / 2f;
-        left += constrain(newZoom, (int) left, mContentRawBounds.width(), mViewport.width());
-        top += constrain(newZoom, (int) top, mContentRawBounds.height(), mViewport.height());
+        left += ZoomUtils.constrainCoordinate(newZoom, (int) left, mContentRawBounds.width(),
+                mViewport.width());
+        top += ZoomUtils.constrainCoordinate(newZoom, (int) top, mContentRawBounds.height(),
+                mViewport.height());
         zoomScrollAnimated(left, top, newZoom, updateListener);
     }
 
@@ -853,24 +836,51 @@
     public void setZoom(float zoom, float pivotX, float pivotY) {
         zoom = Float.isNaN(zoom) ? ZOOM_RESET : zoom;
         mInitialZoomDone = true;
-        int deltaX = scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotX, getScrollX());
-        int deltaY = scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotY, getScrollY());
+        int deltaX = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotX,
+                getScrollX());
+        int deltaY = ZoomUtils.scrollDeltaNeededForZoomChange(getZoom(), zoom, pivotY,
+                getScrollY());
 
         mContentView.setScaleX(zoom);
         mContentView.setScaleY(zoom);
         scrollBy(deltaX, deltaY);
     }
 
+    @NonNull
+    public Rect getZoomViewBasePadding() {
+        return mZoomViewBasePadding;
+    }
+
+    public void setZoomViewBasePadding(@NonNull Rect zoomViewBasePadding) {
+        mZoomViewBasePadding = zoomViewBasePadding;
+    }
+
+    public boolean isZoomViewBasePaddingSaved() {
+        return mZoomViewBasePaddingSaved;
+    }
+
+    public void setZoomViewBasePaddingSaved(boolean zoomViewBasePaddingSaved) {
+        mZoomViewBasePaddingSaved = zoomViewBasePaddingSaved;
+    }
+
+    /** Invokes the [View#setPadding(int, int, int, int)] by adding the base padding */
+    public void setPaddingWithBase(int left, int top, int right, int bottom) {
+        super.setPadding(/* left = */ mZoomViewBasePadding.left + left,
+                /* top = */ mZoomViewBasePadding.top + top,
+                /* right = */ mZoomViewBasePadding.right + right,
+                /* bottom = */ mZoomViewBasePadding.bottom + bottom);
+    }
+
     /**
      * Given a point in the zoom-view's co-ordinates, convert it to the content's co-ordinates,
      * using the current zoom and scroll position of the zoomview.
      */
     protected float toContentX(float zoomViewX) {
-        return toContentCoord(zoomViewX, getZoom(), getScrollX());
+        return ZoomUtils.toContentCoordinate(zoomViewX, getZoom(), getScrollX());
     }
 
     protected float toContentY(float zoomViewY) {
-        return toContentCoord(zoomViewY, getZoom(), getScrollY());
+        return ZoomUtils.toContentCoordinate(zoomViewY, getZoom(), getScrollY());
     }
 
     /**
@@ -878,11 +888,11 @@
      * using the current zoom and scroll position of the zoomview.
      */
     protected float toZoomViewX(float contentX) {
-        return toZoomViewCoord(contentX, getZoom(), getScrollX());
+        return ZoomUtils.toZoomViewCoordinate(contentX, getZoom(), getScrollX());
     }
 
     protected float toZoomViewY(float contentY) {
-        return toZoomViewCoord(contentY, getZoom(), getScrollY());
+        return ZoomUtils.toZoomViewCoordinate(contentY, getZoom(), getScrollY());
     }
 
     /**
@@ -915,11 +925,13 @@
     }
 
     private int constrainX() {
-        return constrain(getZoom(), getScrollX(), mContentRawBounds.width(), mViewport.width());
+        return ZoomUtils.constrainCoordinate(getZoom(), getScrollX(), mContentRawBounds.width(),
+                mViewport.width());
     }
 
     private int constrainY() {
-        return constrain(getZoom(), getScrollY(), mContentRawBounds.height(), mViewport.height());
+        return ZoomUtils.constrainCoordinate(getZoom(), getScrollY(), mContentRawBounds.height(),
+                mViewport.height());
     }
 
     /**
@@ -1055,6 +1067,58 @@
         return new Rect(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
     }
 
+    /**
+     * Saves the padding set on {@link ZoomView} following initial inflation from XML.
+     *
+     * <p>This does not have to be called immediately following inflation but <i>must</i> be called
+     * before any methods change the padding on {@link ZoomView}.
+     *
+     * <p>This can be used by methods that need to set padding to (base padding + some other
+     * dimension). If these values were obtained directly from {@link ZoomView} or this method was
+     * allowed to execute multiple times it could result in padding expanding continually.
+     */
+    public void saveZoomViewBasePadding() {
+        if (mZoomViewBasePaddingSaved) {
+            return;
+        }
+
+        mZoomViewBasePadding =
+                new Rect(
+                        getPaddingLeft(),
+                        getPaddingTop(),
+                        getPaddingRight(),
+                        getPaddingBottom());
+
+        mZoomViewBasePadding.top +=
+                getResources().getDimensionPixelSize(R.dimen.viewer_doc_additional_top_offset);
+
+        mZoomViewBasePaddingSaved = true;
+    }
+
+    /**
+     * Adjusts the horizontal margins (left and right padding) of the ZoomView based on the
+     * screen width to optimize the display of PDF content.
+     *
+     * This method applies different margin values depending on the screen size:
+     * - For screens with a screen width of 840dp or greater, a larger margin is applied
+     * to enhance readability on larger displays.
+     * - For screens with a screen width < 840dp, no margin is used to
+     * maximize the use of available space.
+     *
+     * This dynamic adjustment is achieved through the use of resource qualifiers (values-w840dp)
+     * that define different margin values for different screen sizes.
+     *
+     * Note: This method does not affect the top or bottom padding of the ZoomView.
+     */
+    public void adjustZoomViewMargins() {
+        int margin = getResources().getDimensionPixelSize(R.dimen.viewer_doc_padding_x);
+
+        setPadding(margin,
+                getPaddingTop(),
+                margin,
+                getPaddingBottom());
+    }
+
     /** Different options for the initial zoom that this ZoomView should start with. */
     public static final class InitialZoomMode {
         /** Start with a constant initial zoom, as specified by {@link #setInitialZoom(float)}. */
@@ -1394,15 +1458,19 @@
                 int newScrollX = oldScrollX;
                 int newScrollY = oldScrollY;
                 // Adjust new scroll positions due to changed zoom.
-                newScrollX += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e1.getX(),
+                newScrollX += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom,
+                        e1.getX(),
                         oldScrollX);
-                newScrollY += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e1.getY(),
+                newScrollY += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom,
+                        e1.getY(),
                         oldScrollY);
 
                 // Constrain new scroll positions.
-                newScrollX += constrain(newZoom, newScrollX, mContentRawBounds.width(),
+                newScrollX += ZoomUtils.constrainCoordinate(newZoom, newScrollX,
+                        mContentRawBounds.width(),
                         mViewport.width());
-                newScrollY += constrain(newZoom, newScrollY, mContentRawBounds.height(),
+                newScrollY += ZoomUtils.constrainCoordinate(newZoom, newScrollY,
+                        mContentRawBounds.height(),
                         mViewport.height());
 
                 zoomScrollAnimated(
@@ -1478,15 +1546,17 @@
             int newScrollX = oldScrollX;
             int newScrollY = oldScrollY;
             // Adjust new scroll positions due to changed zoom.
-            newScrollX += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getX(),
+            newScrollX += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getX(),
                     oldScrollX);
-            newScrollY += scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getY(),
+            newScrollY += ZoomUtils.scrollDeltaNeededForZoomChange(currentZoom, newZoom, e.getY(),
                     oldScrollY);
 
             // Constrain new scroll positions.
-            newScrollX += constrain(newZoom, newScrollX, mContentRawBounds.width(),
+            newScrollX += ZoomUtils.constrainCoordinate(newZoom, newScrollX,
+                    mContentRawBounds.width(),
                     mViewport.width());
-            newScrollY += constrain(newZoom, newScrollY, mContentRawBounds.height(),
+            newScrollY += ZoomUtils.constrainCoordinate(newZoom, newScrollY,
+                    mContentRawBounds.height(),
                     mViewport.height());
 
             zoomScrollAnimated(newScrollX, newScrollY, newZoom, null /* updateListener */);
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt
new file mode 100644
index 0000000..bf37790
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/exceptions/PdfPasswordException.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.exceptions
+
+import androidx.annotation.RestrictTo
+import java.lang.SecurityException
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/**
+ * Represents the exception thrown when a password is required or an incorrect password is supplied
+ * to a protected document
+ */
+class PdfPasswordException(message: String) : SecurityException(message)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt
new file mode 100644
index 0000000..d39576e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfDocument.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.loader
+
+import android.graphics.Bitmap
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.pdf.content.PdfPageGotoLinkContent
+import android.graphics.pdf.content.PdfPageImageContent
+import android.graphics.pdf.content.PdfPageLinkContent
+import android.graphics.pdf.content.PdfPageTextContent
+import android.graphics.pdf.models.PageMatchBounds
+import android.graphics.pdf.models.selection.PageSelection
+import android.util.Size
+import android.util.SparseArray
+import androidx.annotation.RestrictTo
+import java.io.Closeable
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/** Represents a PDF document and provides methods to interact with its content. */
+interface PdfDocument : Closeable {
+
+    /** The total number of pages in the document. */
+    val pageCount: Int
+
+    /** Indicates whether the document is linearized (optimized for fast web viewing). */
+    val isLinearized: Boolean
+
+    /** The type of form present in the document. */
+    val formType: Int
+
+    /** Indicates whether the document is password-protected. */
+    val isProtected: Boolean
+
+    /**
+     * Asynchronously retrieves information about the specified page.
+     *
+     * @param pageNumber The page number (0-based).
+     * @return A [PageInfo] object containing information about the page.
+     */
+    suspend fun getPageInfo(pageNumber: Int): PageInfo
+
+    /**
+     * Asynchronously retrieves information about a range of pages.
+     *
+     * @param pageRange The range of page numbers (0-based, inclusive).
+     * @return A list of {@link PageInfo} objects, one for each page in the range.
+     */
+    suspend fun getPageInfos(pageRange: IntRange): List<PageInfo>
+
+    /**
+     * Asynchronously searches the document for the specified query within a range of pages.
+     *
+     * @param query The search query string.
+     * @param pageRange The range of page numbers (0-based, inclusive) to search within.
+     * @return A {@link SparseArray} mapping page numbers to lists of {@link PageMatchBounds}
+     *   objects representing the search results on each page.
+     */
+    suspend fun searchDocument(
+        query: String,
+        pageRange: IntRange
+    ): SparseArray<List<PageMatchBounds>>
+
+    /**
+     * Asynchronously retrieves the selection bounds (in PDF coordinates) for the specified text
+     * selection.
+     *
+     * @param start The starting point of the text selection.
+     * @param stop The ending point of the text selection.
+     * @return A SparseArray mapping page numbers to {@link PageSelection} objects representing the
+     *   selection bounds on each page.
+     */
+    suspend fun getSelectionBounds(start: PdfPoint, stop: PdfPoint): SparseArray<PageSelection>
+
+    /**
+     * Asynchronously retrieves the content (text and images) of the specified page.
+     *
+     * @param pageNumber The page number (0-based).
+     * @return A {@link PdfPageContent} object representing the page's content.
+     */
+    suspend fun getPageContent(pageNumber: Int): PdfPageContent
+
+    /**
+     * Asynchronously retrieves the links (Go To and external) present on the specified page.
+     *
+     * @param pageNumber The page number (0-based).
+     * @return A [PdfPageLinks] object representing the page's links.
+     */
+    suspend fun getPageLinks(pageNumber: Int): PdfPageLinks
+
+    /**
+     * Gets a [BitmapSource] for retrieving bitmap representations of the specified page.
+     *
+     * @param pageNumber The page number (0-based).
+     * @return A [BitmapSource] for the specified page, or null if the page number is invalid.
+     */
+    fun getPageBitmapSource(pageNumber: Int): BitmapSource
+
+    /**
+     * Represents information about a single page in the PDF document.
+     *
+     * @property pageNum The page number (0-based).
+     * @property height The height of the page in points.
+     * @property width The width of the page in points.
+     */
+    class PageInfo(val pageNum: Int, val height: Int, val width: Int)
+
+    /** A source for retrieving bitmap representations of PDF pages. */
+    interface BitmapSource : Closeable {
+        /**
+         * Asynchronously retrieves a bitmap representation of the page, optionally constrained to a
+         * specific tile region.
+         *
+         * @param scaledPageSizePx The scaled page size in pixels, representing the page size in
+         *   case of no zoom, and scaled page size in case of zooming.
+         * @param tileRegion (Optional) The region of the page to include in the bitmap, in pixels
+         *   within the `scaledPageSizePx`. This identifies the tile. If null, the entire page is
+         *   included.
+         * @return The bitmap representation of the page.
+         */
+        suspend fun getBitmap(scaledPageSizePx: Size, tileRegion: Rect? = null): Bitmap
+    }
+
+    /**
+     * Represents the combined text and image content within a single page of a PDF document.
+     *
+     * @property textContents A list of {@link PdfPageTextContent} objects representing the text
+     *   elements on the page.
+     * @property imageContents A list of {@link PdfPageImageContent} objects representing the image
+     *   elements on the page.
+     */
+    class PdfPageContent(
+        val textContents: List<PdfPageTextContent>,
+        val imageContents: List<PdfPageImageContent>
+    )
+
+    /**
+     * Represents the links within a single page of a PDF document.
+     *
+     * @property gotoLinks A list of internal links (links to other pages within the same document)
+     *   represented as `PdfPageGotoLinkContent` objects.
+     * @property externalLinks A list of external links (links to web pages or other resources)
+     *   represented as `PdfPageLinkContent` objects.
+     */
+    class PdfPageLinks(
+        val gotoLinks: List<PdfPageGotoLinkContent>,
+        val externalLinks: List<PdfPageLinkContent>
+    )
+
+    /**
+     * Represents a point within a specific page of a PDF document.
+     *
+     * @property pageNumber The page number (0-based) where the point is located.
+     * @property pagePoint The coordinates (x, y) of the point relative to the page's origin.
+     */
+    class PdfPoint(val pageNumber: Int, val pagePoint: PointF)
+}
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt
new file mode 100644
index 0000000..f8b8e776
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/loader/PdfLoader.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.loader
+
+import android.net.Uri
+import androidx.annotation.RestrictTo
+import androidx.pdf.exceptions.PdfPasswordException
+import java.io.IOException
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/**
+ * Provides an abstraction for asynchronously opening PDF documents from a Uri. Implementations of
+ * this interface are responsible for handling the retrieval, decryption (if necessary), and
+ * creation of a [PdfDocument] representation.
+ */
+interface PdfLoader {
+
+    /**
+     * Asynchronously opens a PDF document from the specified [Uri].
+     *
+     * @param uri The URI of the PDF document to open.
+     * @param password (Optional) The password to unlock the document if it is encrypted.
+     * @return The opened [PdfDocument].
+     * @throws PdfPasswordException If the provided password is incorrect.
+     * @throws IOException If an error occurs while opening the document.
+     */
+    @Throws(PdfPasswordException::class, IOException::class)
+    suspend fun openDocument(uri: Uri, password: String? = null): PdfDocument
+}
diff --git a/pdf/pdf-viewer/src/main/native/CMakeLists.txt b/pdf/pdf-viewer/src/main/native/CMakeLists.txt
deleted file mode 100644
index e687666..0000000
--- a/pdf/pdf-viewer/src/main/native/CMakeLists.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright (C) 2024 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-# For more information about using CMake with Android Studio, read the
-# documentation: https://d.android.com/studio/projects/add-native-code.html.
-# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
-
-# Sets the minimum CMake version required for this project.
-cmake_minimum_required(VERSION 3.22.1)
-
-# Declares the project name. The project name can be accessed via ${PROJECT_NAME}.
-# Since this is the top level CMakeLists.txt, the project name is also accessible
-# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
-# build script scope).
-project("bitmapParcel")
-
-# 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.
-#
-# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
-# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
-# is preferred for the same purpose.
-#
-# In order to load a library into your app from Java/Kotlin, you must call
-# System.loadLibrary() and pass the name of the library defined here;
-# for GameActivity/NativeActivity derived applications, the same library name must be
-# used in the AndroidManifest.xml file.
-add_library(${CMAKE_PROJECT_NAME} SHARED
-        # List C/C++ source files with relative paths to this CMakeLists.txt.
-        bitmap_parcel.cc
-        extractors.cc
-)
-
-# Specifies libraries CMake should link to your target library. You
-# can link libraries from various origins, such as libraries defined in this
-# build script, prebuilt third-party libraries, or Android system libraries.
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log)
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE android)
-target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE jnigraphics)
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc b/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
deleted file mode 100644
index 036437f..0000000
--- a/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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 "bitmap_parcel.h"
-
-#include <android/bitmap.h>
-#include <jni.h>
-#include <stdint.h>
-
-#include "extractors.h"
-#include "logging.h"
-
-#define LOG_TAG "bitmap_parcel"
-
-using pdflib::Extractor;
-using pdflib::BufferReader;
-using pdflib::FdReader;
-
-namespace {
-
-    static const int kBytesPerPixel = 4;
-
-    bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source);
-
-}  // namespace
-
-extern "C" JNIEXPORT jboolean JNICALL
-Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
-        (JNIEnv *env, jclass, jobject jbitmap, int fd) {
-    FdReader source(fd);
-    return FeedBitmap(env, jbitmap, &source);
-}
-
-namespace {
-
-    bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source) {
-        void *bitmap_pixels;
-        int ret;
-        if ((ret = AndroidBitmap_lockPixels(env, jbitmap, &bitmap_pixels)) < 0) {
-            LOGE("AndroidBitmap_lockPixels() failed! error=%d", ret);
-            return false;
-        }
-
-
-        AndroidBitmapInfo info;
-        AndroidBitmap_getInfo(env, jbitmap, &info);
-
-        int num_bytes = info.width * info.height * kBytesPerPixel;
-        uint8_t *bitmap_bytes = reinterpret_cast<uint8_t *>(bitmap_pixels);
-        source->extract(bitmap_bytes, num_bytes);
-
-        AndroidBitmap_unlockPixels(env, jbitmap);
-        LOGV("Copied %d bytes into bitmap", num_bytes);
-        return true;
-    }
-
-}  // namespace
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.h b/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
deleted file mode 100644
index 8f9832e..0000000
--- a/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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>
-
-#ifndef ANDROIDX_PDF_BITMAP_PARCEL_H_
-#define ANDROIDX_PDF_BITMAP_PARCEL_H_
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-JNIEXPORT jboolean JNICALL
-Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
-        (JNIEnv *env, jclass, jobject jbitmap, jint jfd);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif  // ANDROIDX_PDF_BITMAP_PARCEL_H_
diff --git a/pdf/pdf-viewer/src/main/native/extractors.cc b/pdf/pdf-viewer/src/main/native/extractors.cc
deleted file mode 100644
index c7ead25..0000000
--- a/pdf/pdf-viewer/src/main/native/extractors.cc
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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 "extractors.h"
-
-#include <stdint.h>
-#include <unistd.h>
-
-#include <cstring>
-
-#include "logging.h"
-
-#define LOG_TAG "extractor"
-
-namespace pdflib {
-
-    Extractor::~Extractor() {}
-
-    BufferWriter::~BufferWriter() {}
-
-    bool BufferWriter::extract(uint8_t *source, int num_bytes) {
-        memcpy(buffer_, source, num_bytes);
-        return true;
-    }
-
-    BufferReader::~BufferReader() {}
-
-    bool BufferReader::extract(uint8_t *destination, int num_bytes) {
-        memcpy(destination, buffer_, num_bytes);
-        return true;
-    }
-
-    FdWriter::~FdWriter() {}
-
-    bool FdWriter::extract(uint8_t *source, int num_bytes) {
-        LOGV("FdWriter Extracting %d bytes on %d", num_bytes, fd_);
-        bool ret = true;
-        while (num_bytes > 0) {
-            int len = write(fd_, source, num_bytes);
-            if (len == -1 || len == 0) {
-                ret = false;
-                LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
-                break;
-            }
-            num_bytes -= len;
-            source += len;
-        }
-        close(fd_);
-        return ret;
-    }
-
-    FdReader::~FdReader() {}
-
-    bool FdReader::extract(uint8_t *destination, int num_bytes) {
-        LOGV("FdReader Extracting %d bytes from %d", num_bytes, fd_);
-        bool ret = true;
-        while (num_bytes > 0) {
-            int len = read(fd_, destination, num_bytes);
-            if (len == -1 || len == 0) {
-                ret = false;
-                LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
-                break;
-            }
-            num_bytes -= len;
-            destination += len;
-        }
-        close(fd_);
-        return ret;
-    }
-
-}  // namespace pdflib
diff --git a/pdf/pdf-viewer/src/main/native/extractors.h b/pdf/pdf-viewer/src/main/native/extractors.h
deleted file mode 100644
index 493ac51..0000000
--- a/pdf/pdf-viewer/src/main/native/extractors.h
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef ANDROIDX_PDF_EXTRACTORS_H
-#define ANDROIDX_PDF_EXTRACTORS_H
-
-#include <stdint.h>
-
-namespace pdflib {
-
-// Interface for extracting bytes from or into an underlying something.
-    class Extractor {
-    public:
-        // Transfers {num_bytes} bytes between the underlying something and {buffer}.
-        virtual ~Extractor();
-
-        virtual bool extract(uint8_t *buffer, int num_bytes) = 0;
-    };
-
-
-// An Extractor that copies bytes on the given buffer.
-    class BufferWriter : public pdflib::Extractor {
-    public:
-        explicit BufferWriter(uint8_t *buffer) : buffer_(buffer) {}
-
-        ~BufferWriter() override;
-
-        bool extract(uint8_t *source, int num_bytes) override;
-
-    private:
-        uint8_t *buffer_;
-    };
-
-// An Extractor that copies bytes from the given buffer.
-    class BufferReader : public pdflib::Extractor {
-    public:
-        explicit BufferReader(uint8_t *buffer) : buffer_(buffer) {}
-
-        ~BufferReader() override;
-
-        bool extract(uint8_t *source, int num_bytes) override;
-
-    private:
-        uint8_t *buffer_;
-    };
-
-// An extractor that writes bytes on the given fd. It closes the fd thereafter.
-    class FdWriter : public pdflib::Extractor {
-    public:
-        explicit FdWriter(int fd) : fd_(fd) {}
-
-        ~FdWriter() override;
-
-        bool extract(uint8_t *source, int num_bytes) override;
-
-    private:
-        int fd_;
-    };
-
-// An extractor that read bytes from the given fd. It closes the fd thereafter.
-    class FdReader : public pdflib::Extractor {
-    public:
-        explicit FdReader(int fd) : fd_(fd) {}
-
-        ~FdReader() override;
-
-        bool extract(uint8_t *destination, int num_bytes) override;
-
-    private:
-        int fd_;
-    };
-
-}  // namespace pdfClient
-
-#endif  // ANDROIDX_PDF_EXTRACTORS_H
diff --git a/pdf/pdf-viewer/src/main/native/logging.h b/pdf/pdf-viewer/src/main/native/logging.h
deleted file mode 100644
index a04bed4..0000000
--- a/pdf/pdf-viewer/src/main/native/logging.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef ANDROIDX_PDF_LOGGING_H
-#define ANDROIDX_PDF_LOGGING_H
-
-#include <android/log.h>
-
-#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
-#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
-#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
-
-#endif //ANDROIDX_PDF_LOGGING_H
diff --git a/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml b/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml
deleted file mode 100644
index c543141..0000000
--- a/pdf/pdf-viewer/src/main/res/values-night-v31/colors.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  Copyright 2023 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-
-    <color name="google_blue">#ff1a73e8</color>
-    <color name="google_white">#ffffffff</color>
-    <color name="google_grey">#ff3c4043</color>
-    <color name="text_default">#666</color>
-    <color name="text_error">#da4336</color>
-    <color name="selection_handles">#00aadd</color>
-    <color name="search_background">@color/material_dynamic_neutral_variant10</color>
-    <color name="search_textbox">@color/material_dynamic_neutral_variant20</color>
-    <color name="search_texthint">@android:color/system_neutral2_200</color>
-    <color name="search_textColor">@android:color/system_neutral1_100</color>
-    <color name="search_count">@android:color/system_neutral2_200</color>
-    <color name="search_prev_button">@android:color/system_neutral2_200</color>
-    <color name="search_next_button">@android:color/system_neutral2_200</color>
-    <color name="search_close_button">@android:color/system_neutral2_200</color>
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/res/values-v31/colors.xml b/pdf/pdf-viewer/src/main/res/values-v31/colors.xml
deleted file mode 100644
index 22db258..0000000
--- a/pdf/pdf-viewer/src/main/res/values-v31/colors.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
-  Copyright 2023 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<resources>
-
-    <color name="google_blue">#ff1a73e8</color>
-    <color name="google_white">#ffffffff</color>
-    <color name="google_grey">#ff3c4043</color>
-    <color name="text_default">#666</color>
-    <color name="text_error">#da4336</color>
-    <color name="selection_handles">#00aadd</color>
-    <color name="search_background">@color/material_dynamic_neutral_variant95</color>
-    <color name="search_textbox">@color/material_dynamic_neutral_variant99</color>
-    <color name="search_texthint">@android:color/system_neutral2_700</color>
-    <color name="search_textColor">@android:color/system_neutral1_900</color>
-    <color name="search_count">@android:color/system_neutral2_700</color>
-    <color name="search_prev_button">@android:color/system_neutral2_700</color>
-    <color name="search_next_button">@android:color/system_neutral2_700</color>
-    <color name="search_close_button">@android:color/system_neutral2_700</color>
-
-</resources>
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java
deleted file mode 100644
index 01442a8..0000000
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/mocks/MockBitmapParcel.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.pdf.mocks;
-
-import android.graphics.Bitmap;
-import android.os.ParcelFileDescriptor;
-
-import androidx.pdf.util.BitmapParcel;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-
-public class MockBitmapParcel extends BitmapParcel {
-
-    private final int mBufferSize;
-
-    public MockBitmapParcel(Bitmap bitmap, int bufferSize) {
-        super(bitmap);
-        this.mBufferSize = bufferSize;
-    }
-
-    @Override
-    protected void receiveBitmap(Bitmap bitmap, ParcelFileDescriptor sourceFd) {
-        try (FileInputStream fis = new FileInputStream(sourceFd.getFileDescriptor())) {
-            FileChannel channel = fis.getChannel();
-
-            ByteBuffer buffer = ByteBuffer.allocateDirect(mBufferSize);
-            while (channel.read(buffer) != -1) {
-                buffer.rewind();
-                bitmap.copyPixelsFromBuffer(buffer);
-            }
-            buffer.rewind();
-            channel.close();
-            sourceFd.close();
-        } catch (IOException ignored) {
-        }
-    }
-}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
index 4b29401..63f84b5 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/AnnotationUtilsTest.java
@@ -66,7 +66,7 @@
     }
 
     @Test
-    public void launchAnnotationIntent_nonNullUri_returnsTrue() {
+    public void resolveAnnotationIntent_nonNullUri_returnsTrue() {
         String fileName = "file:://dummyfile.pdf";
         Uri uri = Uri.fromFile(new File(fileName));
 
@@ -85,23 +85,23 @@
                 mockResolveInfo);
         when(mockContext.getPackageManager()).thenReturn(mockPackageManager);
 
-        boolean result = AnnotationUtils.launchAnnotationIntent(mockContext, uri);
+        boolean result = AnnotationUtils.resolveAnnotationIntent(mockContext, uri);
         assertTrue(result);
     }
 
     @Test
-    public void launchAnnotationIntent_nullContext_returnsNullPointerException() {
+    public void resolveAnnotationIntent_nullContext_returnsNullPointerException() {
         assertThrows(NullPointerException.class,
-                () -> AnnotationUtils.launchAnnotationIntent(
+                () -> AnnotationUtils.resolveAnnotationIntent(
                         ApplicationProvider.getApplicationContext(), null));
     }
 
     @Test
-    public void launchAnnotationIntent_nullUri_returnsNullPointerException() {
+    public void resolveAnnotationIntent_nullUri_returnsNullPointerException() {
         String fileName = "file:://dummyfile.pdf";
         Uri uri = Uri.fromFile(new File(fileName));
         assertThrows(NullPointerException.class,
-                () -> AnnotationUtils.launchAnnotationIntent(
+                () -> AnnotationUtils.resolveAnnotationIntent(
                         null, uri));
     }
 }
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
deleted file mode 100644
index 91db95d..0000000
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/BitmapParcelTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.pdf.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.ParcelFileDescriptor;
-
-import androidx.pdf.mocks.MockBitmapParcel;
-import androidx.pdf.test.R;
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-
-import java.io.ByteArrayInputStream;
-import java.io.OutputStream;
-import java.nio.Buffer;
-import java.nio.ByteBuffer;
-
-//@SmallTest
-//@RunWith(RobolectricTestRunner.class)
-public class BitmapParcelTest {
-
-    private static final String TAG = "BitmapParcelTest";
-
-    private Bitmap mSourceBitmap;
-
-    @Before
-    public void setUp() {
-        mSourceBitmap =
-                BitmapFactory.decodeResource(
-                        ApplicationProvider.getApplicationContext().getResources(),
-                        R.raw.launcher_pdfviewer);
-    }
-
-    // TODO: Fails in the first execution and then passes. Uncomment when fixed
-//    @Test
-//    public void testBitmapTransferWithOutputFileDescriptor() {
-//        int bufferSize = mSourceBitmap.getWidth() * mSourceBitmap.getHeight() * 256;
-//        testBitmapTransfer(bufferSize);
-//    }
-
-    private void testBitmapTransfer(int bufferSize) {
-        Bitmap bitmap = Bitmap.createBitmap(mSourceBitmap.getWidth(), mSourceBitmap.getHeight(),
-                mSourceBitmap.getConfig());
-        BitmapParcel bitmapParcel = new MockBitmapParcel(bitmap, bufferSize);
-        ParcelFileDescriptor fd = bitmapParcel.openOutputFd();
-        if (fd == null) {
-            fail("fd is null");
-        }
-        sendBytes(fd);
-        bitmapParcel.close();
-        assertThat(validateBitmap(bitmap)).isTrue();
-    }
-
-    private boolean validateBitmap(Bitmap bitmap) {
-        boolean same = bitmap.sameAs(mSourceBitmap);
-        return same;
-    }
-
-    private void sendBytes(OutputStream os) {
-        int numBytes = mSourceBitmap.getByteCount();
-        byte[] bytes = new byte[numBytes];
-        Buffer buffer = ByteBuffer.wrap(bytes);
-        mSourceBitmap.copyPixelsToBuffer(buffer);
-        ByteArrayInputStream is = new ByteArrayInputStream(bytes);
-        Utils.copyAndClose(is, os);
-    }
-
-
-    private void sendBytes(ParcelFileDescriptor fd) {
-        OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
-        int numBytes = mSourceBitmap.getByteCount();
-        byte[] bytes = new byte[numBytes];
-        Buffer buffer = ByteBuffer.wrap(bytes);
-        mSourceBitmap.copyPixelsToBuffer(buffer);
-        ByteArrayInputStream is = new ByteArrayInputStream(bytes);
-        Utils.copyAndClose(is, os);
-    }
-}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
index 6f76315..e385bae 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/ZoomUtilsTest.java
@@ -18,6 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
@@ -52,4 +56,146 @@
         assertThat(ZoomUtils.calculateZoomToFit(20, 2, 1, 0)).isEqualTo(1f);
         assertThat(ZoomUtils.calculateZoomToFit(0, 0, 1, 2)).isEqualTo(0f);
     }
+
+    @Test
+    public void toContentCoordinate_nonZeroZoomScroll_returnsNonZeroCoordinate() {
+        // Arrange
+        float dummyZoomViewCoordinate = 10f;
+        float dummyZoom = 2f;
+        int dummyScroll = 5;
+        float expectedContentCoordinate = 7.5f;
+
+        // Act
+        float contentCoordinate = ZoomUtils.toContentCoordinate(dummyZoomViewCoordinate,
+                dummyZoom, dummyScroll);
+
+        // Assert
+        assertThat(contentCoordinate).isEqualTo(expectedContentCoordinate);
+    }
+
+    @Test
+    public void toContentCoordinate_zeroZoom_throwsIllegalArgumentException() {
+        float dummyZoomViewCoordinate = 10f;
+        float zoom = 0;
+        int dummyScroll = 10;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> ZoomUtils.toContentCoordinate(dummyZoomViewCoordinate, zoom,
+                        dummyScroll));
+    }
+
+    @Test
+    public void toZoomViewCoordinate_nonZeroZoomScroll_returnsNonZeroCoordinate() {
+        float dummyContentCoordinate = 10f;
+        float dummyZoom = 2f;
+        int dummyScroll = 5;
+        float expectedZoomViewCoordinate = 15f;
+
+        float zoomViewCoordinate = ZoomUtils.toZoomViewCoordinate(dummyContentCoordinate,
+                dummyZoom, dummyScroll);
+
+        assertThat(zoomViewCoordinate).isEqualTo(expectedZoomViewCoordinate);
+    }
+
+    @Test
+    public void toZoomViewCoordinate_zeroZoom_throwsIllegalArgumentException() {
+        float dummyContentCoordinate = 10f;
+        float zoom = 0;
+        int dummyScroll = 10;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> ZoomUtils.toZoomViewCoordinate(dummyContentCoordinate, zoom,
+                        dummyScroll));
+    }
+
+    @Test
+    public void scrollDeltaNeededForZoomChange_noZoomChange_returnsZeroDelta() {
+        float oldZoom = 1f;
+        float newZoom = 1f;
+        float zoomPivot = 5f;
+        int scroll = 1;
+
+        int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+                scroll);
+
+        assertEquals(0, delta);
+    }
+
+    @Test
+    public void scrollDeltaNeededForZoomChange_zoomedIn_returnsPositiveDelta() {
+        float oldZoom = 1f;
+        float newZoom = 3f;
+        float zoomPivot = 5f;
+        int scroll = 1;
+
+        int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+                scroll);
+
+        assertTrue(delta >= 0);
+    }
+
+    @Test
+    public void scrollDeltaNeededForZoomChange_zoomedOut_returnsNegativeDelta() {
+        float oldZoom = 2f;
+        float newZoom = 1f;
+        float zoomPivot = 5f;
+        int scroll = 1;
+
+        int delta = ZoomUtils.scrollDeltaNeededForZoomChange(oldZoom, newZoom, zoomPivot,
+                scroll);
+
+        assertTrue(delta <= 0);
+    }
+
+    @Test
+    public void constrainCoordinate_contentLargerThanViewport_returnsZero() {
+        float zoom = 1f;
+        int scroll = 0;
+        int rawContentDimension = 5;
+        int viewportDimension = 5;
+
+        int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+                rawContentDimension, viewportDimension);
+
+        assertEquals(0, adjustedCoordinate);
+    }
+
+    @Test
+    public void constrainCoordinate_scaledContentWithinViewport_returnsNegativeAdjustedCoord() {
+        float zoom = 1f;
+        int scroll = 0;
+        int rawContentDimension = 6;
+        int viewportDimension = 8;
+
+        int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+                rawContentDimension, viewportDimension);
+
+        assertTrue(adjustedCoordinate <= 0);
+    }
+
+    @Test
+    public void constrainCoordinate_contentWithLeftDeadMargins_returnsPositiveAdjustedCoord() {
+        float zoom = 1f;
+        int scroll = -2;
+        int rawContentDimension = 6;
+        int viewportDimension = 5;
+
+        int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+                rawContentDimension, viewportDimension);
+
+        assertTrue(adjustedCoordinate >= 0);
+    }
+
+    @Test
+    public void constrainCoordinate_contentWithRightDeadMargin_returnsNegativeAdjustedCoord() {
+        float zoom = 1f;
+        int scroll = 2;
+        int rawContentDimension = 6;
+        int viewportDimension = 5;
+
+        int adjustedCoordinate = ZoomUtils.constrainCoordinate(zoom, scroll,
+                rawContentDimension, viewportDimension);
+
+        assertTrue(adjustedCoordinate <= 0);
+    }
 }
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java
new file mode 100644
index 0000000..2e1dd3b
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/FastScrollPositionValueObserverTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.view.View;
+
+import androidx.pdf.widget.FastScrollView;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class FastScrollPositionValueObserverTest {
+    private final FastScrollView mMockFastScrollView = mock(FastScrollView.class);
+    private final PageIndicator mMockPageIndicator = mock(PageIndicator.class);
+    private final View mMockView = mock(View.class);
+
+    @Test
+    public void onChange() {
+        when(mMockPageIndicator.getView()).thenReturn(mMockView);
+        FastScrollPositionValueObserver fastScrollPositionValueObserver =
+                new FastScrollPositionValueObserver(mMockFastScrollView, mMockPageIndicator);
+        fastScrollPositionValueObserver.onChange(null, 100);
+        verify(mMockPageIndicator).show();
+        verify(mMockPageIndicator, times(2)).getView();
+        verify(mMockFastScrollView).setVisible();
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
index 0a45c71..7b070fc 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityDisabledFactory.java
@@ -29,8 +29,9 @@
     public MockPageViewAccessbilityDisabledFactory(@NonNull Context context,
             @NonNull PdfLoader pdfLoader,
             @NonNull PaginatedView paginatedView,
-            @NonNull ZoomView zoomView) {
-        super(context, pdfLoader, paginatedView, zoomView);
+            @NonNull ZoomView zoomView,
+            @NonNull SingleTapHandler singleTapHandler) {
+        super(context, pdfLoader, paginatedView, zoomView, singleTapHandler);
     }
 
     @NonNull
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
index 1dd7e58..cb51642 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/MockPageViewAccessbilityEnabledFactory.java
@@ -31,8 +31,9 @@
             @NonNull Context context,
             @NonNull PdfLoader pdfLoader,
             @NonNull PaginatedView paginatedView,
-            @NonNull ZoomView zoomView) {
-        super(context, pdfLoader, paginatedView, zoomView);
+            @NonNull ZoomView zoomView,
+            @NonNull SingleTapHandler singleTapHandler) {
+        super(context, pdfLoader, paginatedView, zoomView, singleTapHandler);
     }
 
     @NonNull
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
index 0832ad5..47cf7c0e 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageMosaicViewTest.java
@@ -18,11 +18,24 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Rect;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.models.SelectionBoundary;
 import androidx.pdf.util.BitmapRecycler;
 import androidx.pdf.util.MockDrawable;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.pdf.widget.MosaicView;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
@@ -33,6 +46,8 @@
 import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 
+import java.util.List;
+
 /** Unit tests for {@link PageMosaicView}. */
 @SmallTest
 @RunWith(RobolectricTestRunner.class)
@@ -43,6 +58,15 @@
     @Mock
     private BitmapRecycler mMockBitmapRecycler;
 
+    @Mock
+    private PdfLoader mMockPdfLoader;
+
+    @Mock
+    private PdfSelectionModel mPdfSelectionModel;
+
+    @Mock
+    private SearchModel mSearchModel;
+
     @Before
     public void setup() {
 
@@ -53,7 +77,8 @@
         Dimensions dimensions = new Dimensions(800, 800);
         PageMosaicView pageMosaicView = new PageMosaicView(
                 ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
-                mMockBitmapSource, mMockBitmapRecycler);
+                mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+                mSearchModel);
 
         assertThat(pageMosaicView.getPageText() == null).isTrue();
     }
@@ -63,7 +88,8 @@
         Dimensions dimensions = new Dimensions(800, 800);
         PageMosaicView pageMosaicView = new PageMosaicView(
                 ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
-                mMockBitmapSource, mMockBitmapRecycler);
+                mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+                mSearchModel);
 
         pageMosaicView.setOverlay(new MockDrawable());
         assertThat(pageMosaicView.hasOverlay()).isTrue();
@@ -74,7 +100,8 @@
         Dimensions dimensions = new Dimensions(800, 800);
         PageMosaicView pageMosaicView = new PageMosaicView(
                 ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
-                mMockBitmapSource, mMockBitmapRecycler);
+                mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+                mSearchModel);
 
         pageMosaicView.setOverlay(new MockDrawable());
         assertThat(pageMosaicView.hasOverlay()).isTrue();
@@ -88,7 +115,8 @@
         Dimensions dimensions = new Dimensions(800, 800);
         PageMosaicView pageMosaicView = new PageMosaicView(
                 ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
-                mMockBitmapSource, mMockBitmapRecycler) {
+                mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+                mSearchModel) {
             @Override
             @NonNull
             protected String buildContentDescription(@Nullable String pageText, int pageNum) {
@@ -108,7 +136,8 @@
         Dimensions dimensions = new Dimensions(800, 800);
         PageMosaicView pageMosaicView = new PageMosaicView(
                 ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
-                mMockBitmapSource, mMockBitmapRecycler) {
+                mMockBitmapSource, mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel,
+                mSearchModel) {
             @Override
             @NonNull
             protected String buildContentDescription(@Nullable String pageText, int pageNum) {
@@ -120,4 +149,133 @@
         assertThat(pageMosaicView.getPageText() == null).isTrue();
         assertThat(pageMosaicView.getContentDescription()).isEqualTo(dummyContentDescription);
     }
+
+    @Test
+    public void refreshPageContentAndOverlays_needsPageText_callsLoadPageTextRemovesOverlay() {
+        PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+        PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+        SearchModel dummySearchModel = mock(SearchModel.class);
+        when(dummyPdfSelectionModel.getPage()).thenReturn(1);
+        when(dummySearchModel.query()).thenReturn(new ObservableValue<String>() {
+            @Nullable
+            @Override
+            public String get() {
+                return null;
+            }
+
+            @NonNull
+            @Override
+            public Object addObserver(ValueObserver<String> observer) {
+                return null;
+            }
+
+            @Override
+            public void removeObserver(@NonNull Object observerKey) {
+
+            }
+        });
+
+        Dimensions dimensions = new Dimensions(800, 800);
+        PageMosaicView pageMosaicView = new PageMosaicView(
+                ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+                mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+                dummySearchModel) {
+            @Override
+            public boolean needsPageText() {
+                return true;
+            }
+        };
+
+        pageMosaicView.refreshPageContentAndOverlays();
+
+        verify(dummyPdfLoader).loadPageText(any(Integer.class));
+        verify(dummyPdfLoader).loadPageUrlLinks(any(Integer.class));
+        verify(dummyPdfLoader).loadPageGotoLinks(any(Integer.class));
+        assertFalse(pageMosaicView.hasOverlay());
+    }
+
+    @Test
+    public void refreshPageContentAndOverlays_sameSelectionPage_setsHighlightOverlay() {
+        List<Rect> boundingBoxes = List.of(new Rect(10, 10, 10, 10));
+        PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+        PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+        SearchModel dummySearchModel = mock(SearchModel.class);
+        when(dummyPdfSelectionModel.getPage()).thenReturn(0);
+        when(dummyPdfSelectionModel.selection()).thenReturn(new ObservableValue<PageSelection>() {
+
+            @NonNull
+            @Override
+            public Object addObserver(ValueObserver<PageSelection> observer) {
+                return null;
+            }
+
+            @Override
+            public void removeObserver(@NonNull Object observerKey) {
+
+            }
+
+            @Nullable
+            @Override
+            public PageSelection get() {
+                return new PageSelection(0, mock(SelectionBoundary.class),
+                        mock(SelectionBoundary.class), boundingBoxes, "");
+            }
+        });
+
+        Dimensions dimensions = new Dimensions(800, 800);
+        PageMosaicView pageMosaicView = new PageMosaicView(
+                ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+                mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+                dummySearchModel) {
+            @Override
+            public boolean needsPageText() {
+                return false;
+            }
+        };
+
+        pageMosaicView.refreshPageContentAndOverlays();
+
+        assertTrue(pageMosaicView.hasOverlay());
+    }
+
+    @Test
+    public void refreshPageContentAndOverlays_nonNullSearchQuery_callsSearchPageText() {
+        PdfLoader dummyPdfLoader = mock(PdfLoader.class);
+        PdfSelectionModel dummyPdfSelectionModel = mock(PdfSelectionModel.class);
+        SearchModel dummySearchModel = mock(SearchModel.class);
+        when(dummyPdfSelectionModel.getPage()).thenReturn(1);
+        when(dummySearchModel.query()).thenReturn(new ObservableValue<String>() {
+            @Nullable
+            @Override
+            public String get() {
+                return "placeholder";
+            }
+
+            @NonNull
+            @Override
+            public Object addObserver(ValueObserver<String> observer) {
+                return null;
+            }
+
+            @Override
+            public void removeObserver(@NonNull Object observerKey) {
+
+            }
+        });
+
+        Dimensions dimensions = new Dimensions(800, 800);
+        PageMosaicView pageMosaicView = new PageMosaicView(
+                ApplicationProvider.getApplicationContext(), /* pageNum= */ 0, dimensions,
+                mMockBitmapSource, mMockBitmapRecycler, dummyPdfLoader, dummyPdfSelectionModel,
+                dummySearchModel) {
+            @Override
+            public boolean needsPageText() {
+                return false;
+            }
+        };
+
+        pageMosaicView.refreshPageContentAndOverlays();
+
+        verify(dummyPdfLoader).searchPageText(any(Integer.class), any(String.class));
+    }
 }
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java
new file mode 100644
index 0000000..da59d113
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageRangeHandlerTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import androidx.pdf.data.Range;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class PageRangeHandlerTest {
+    @Test
+    public void getVisiblePage_nullVisiblePageRange_returnsZero() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        int expectedResult = 0;
+
+        int result = adapter.getVisiblePage();
+
+        assertThat(result).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void getVisiblePage_nonNullRange_returnsMidOfRange() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        Range dummyVisiblePageRange = new Range(1, 4);
+        int expectedResult = 2;
+
+        adapter.setVisiblePages(dummyVisiblePageRange);
+        int result = adapter.getVisiblePage();
+
+        assertThat(result).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void adjustMaxPageToUpperVisibleRange_nullVisiblePageRange_noChangeInMaxPage() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        int expectedResult = -1;
+
+        adapter.adjustMaxPageToUpperVisibleRange();
+        int result = adapter.getMaxPage();
+
+        assertThat(result).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void adjustMaxPageToUpperVisibleRange_lowerUpperVisibleRange_noChangeInMaxPage() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        Range dummyVisiblePageRange = new Range(1, 4);
+        int dummyMaxPage = 10;
+
+        adapter.setVisiblePages(dummyVisiblePageRange);
+        adapter.setMaxPage(dummyMaxPage);
+        adapter.adjustMaxPageToUpperVisibleRange();
+        int result = adapter.getMaxPage();
+
+        assertThat(result).isEqualTo(dummyMaxPage);
+    }
+
+    @Test
+    public void adjustMaxPageToUpperVisibleRange_higherUpperVisibleRange_updatedMaxPage() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        Range dummyVisiblePageRange = new Range(1, 10);
+        int dummyMaxPage = 4;
+        int expectedResult = 10;
+
+        adapter.setVisiblePages(dummyVisiblePageRange);
+        adapter.setMaxPage(dummyMaxPage);
+        adapter.adjustMaxPageToUpperVisibleRange();
+        int result = adapter.getMaxPage();
+
+        assertThat(result).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void computeVisibleRange_zeroZoom_throwsIllegalArgumentException() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        int dummyScrollY = 10;
+        float dummyZoom = 0;
+        int dummyViewHeight = 100;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> adapter.computeVisibleRange(dummyScrollY, dummyZoom, dummyViewHeight, true));
+    }
+
+    @Test
+    public void computeVisibleRange_overlappingRange_returnsOverlappedRange() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        when(mockPaginationModel.getPagesInWindow(any(Range.class), any(Boolean.class))).thenAnswer(
+                new Answer<Range>() {
+                    private final Range mDefaultRange = new Range(3, 5);
+
+                    @Override
+                    public Range answer(InvocationOnMock invocation) throws Throwable {
+                        Range intervalPx = invocation.getArgument(0);
+                        boolean includePartial = invocation.getArgument(1);
+
+                        // Condition has been added so that we can capture a change to the value
+                        if (includePartial) {
+                            return new Range(
+                                    Math.min(intervalPx.getFirst(), mDefaultRange.getFirst()),
+                                    Math.max(intervalPx.getLast(), mDefaultRange.getLast())
+                            );
+                        }
+                        return mDefaultRange;
+
+                    }
+                });
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        int dummyScrollY = 2;
+        float dummyZoom = 2;
+        int dummyViewHeight = 10;
+        Range expectedResult = new Range(1, 6);
+
+        Range result = adapter.computeVisibleRange(dummyScrollY, dummyZoom, dummyViewHeight, true);
+
+        assertEquals(result, expectedResult);
+    }
+
+    @Test
+    public void refreshVisiblePageRange_overlappingRange_updatesVisibleRangeWithOverlappedRange() {
+        PaginationModel mockPaginationModel = Mockito.mock(PaginationModel.class);
+        when(mockPaginationModel.getPagesInWindow(any(Range.class), any(Boolean.class))).thenAnswer(
+                new Answer<Range>() {
+                    private final Range mDefaultRange = new Range(3, 5);
+
+                    @Override
+                    public Range answer(InvocationOnMock invocation) throws Throwable {
+                        Range intervalPx = invocation.getArgument(0);
+                        boolean includePartial = invocation.getArgument(1);
+
+                        // Condition has been added so that we can capture a change to the value
+                        if (includePartial) {
+                            return new Range(
+                                    Math.min(intervalPx.getFirst(), mDefaultRange.getFirst()),
+                                    Math.max(intervalPx.getLast(), mDefaultRange.getLast())
+                            );
+                        }
+                        return mDefaultRange;
+
+                    }
+                });
+        PageRangeHandler adapter = new PageRangeHandler(mockPaginationModel);
+        int dummyScrollY = 2;
+        float dummyZoom = 2;
+        int dummyViewHeight = 10;
+        Range expectedResult = new Range(1, 6);
+
+        adapter.refreshVisiblePageRange(dummyScrollY, dummyZoom, dummyViewHeight);
+        Range result = adapter.getVisiblePages();
+
+        assertEquals(result, expectedResult);
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
new file mode 100644
index 0000000..1927d8b
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageSelectionValueObserverTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.PageSelection;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class PageSelectionValueObserverTest {
+    private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+    private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+    private final PageViewFactory mMockPageViewFactory = mock(PageViewFactory.class);
+    private final PageSelection mMockOldPageSelection = mock(PageSelection.class);
+    private final PageSelection mMockNewPageSelection = mock(PageSelection.class);
+    private final PageViewFactory.PageView mMockOldPageView = mock(PageViewFactory.PageView.class);
+    private final PageViewFactory.PageView mMockNewPageView = mock(PageViewFactory.PageView.class);
+    private final PageMosaicView mMockOldPageMosaicView = mock(PageMosaicView.class);
+    private final PageMosaicView mMockNewPageMosaicView = mock(PageMosaicView.class);
+    private final PageRangeHandler mMockPageRangeHandler = mock(PageRangeHandler.class);
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Test
+    public void onChange_setOverlay() {
+        when(mMockOldPageSelection.getPage()).thenReturn(1);
+        when(mMockNewPageSelection.getPage()).thenReturn(2);
+        when(mMockPaginationModel.getSize()).thenReturn(3);
+        when(mMockPaginationModel.getPageSize(2)).thenReturn(new Dimensions(100, 100));
+        when(mMockPaginatedView.getViewAt(1)).thenReturn(mMockOldPageView);
+        when(mMockPaginatedView.getViewAt(2)).thenReturn(mMockNewPageView);
+        when(mMockOldPageView.getPageView()).thenReturn(mMockOldPageMosaicView);
+        when(mMockPageViewFactory.getOrCreatePageView(2, 2, new Dimensions(100, 100))).thenReturn(
+                mMockNewPageMosaicView);
+        when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mMockPageRangeHandler);
+        when(mMockPageRangeHandler.getVisiblePages()).thenReturn(new Range(1, 2));
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        displayMetrics.density = 1f;
+        mContext.getResources().getDisplayMetrics().setTo(displayMetrics);
+        PdfViewer.setScreenForTest(mContext);
+
+        PageSelectionValueObserver pageSelectionValueObserver =
+                new PageSelectionValueObserver(mMockPaginatedView, mMockPaginationModel,
+                        mMockPageViewFactory, mContext);
+        pageSelectionValueObserver.onChange(mMockOldPageSelection, mMockNewPageSelection);
+
+        verify(mMockOldPageMosaicView).setOverlay(null);
+        verify(mMockNewPageMosaicView).setOverlay(any());
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
index d14175d..c3869d4 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PageViewFactoryTest.java
@@ -50,8 +50,7 @@
 
     private final ZoomView mMockZoomView = mock(ZoomView.class);
 
-    private final PdfViewer.PageTouchHandler mMockPageTouchHandler = mock(
-            PdfViewer.PageTouchHandler.class);
+    private final SingleTapHandler mMockSingleTapHandler = mock(SingleTapHandler.class);
 
     @Before
     public void setup() {
@@ -69,12 +68,11 @@
                 PageViewFactory.PageView.class);
         PageViewFactory mockPageViewFactory = new MockPageViewAccessbilityDisabledFactory(
                 ApplicationProvider.getApplicationContext(), mMockPdfLoader, mMockPaginatedView,
-                mMockZoomView
+                mMockZoomView, mMockSingleTapHandler
         );
 
         // Act
-        mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions,
-                mMockPageTouchHandler);
+        mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions);
 
         // Assert
         verify(mMockPaginatedView).addView(pageViewArgumentCaptor.capture());
@@ -101,12 +99,11 @@
                 PageViewFactory.PageView.class);
         PageViewFactory mockPageViewFactory = new MockPageViewAccessbilityEnabledFactory(
                 ApplicationProvider.getApplicationContext(), mMockPdfLoader, mMockPaginatedView,
-                mMockZoomView
+                mMockZoomView, mMockSingleTapHandler
         );
 
         // Act
-        mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions,
-                mMockPageTouchHandler);
+        mockPageViewFactory.getOrCreatePageView(0, dummyPageElevation, mockDimensions);
 
         // Assert
         verify(mMockPaginatedView).addView(pageViewArgumentCaptor.capture());
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
index a7f335d..1ebf1c7 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/PaginatedViewTest.java
@@ -23,6 +23,7 @@
 import androidx.pdf.models.Dimensions;
 import androidx.pdf.util.BitmapRecycler;
 import androidx.pdf.viewer.PageViewFactory.PageView;
+import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.pdf.widget.MosaicView.BitmapSource;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
@@ -52,6 +53,12 @@
     BitmapSource mMockBitmapSource;
     @Mock
     BitmapRecycler mMockBitmapRecycler;
+    @Mock
+    PdfLoader mMockPdfLoader;
+    @Mock
+    PdfSelectionModel mPdfSelectionModel;
+    @Mock
+    SearchModel mSearchModel;
 
     PageView mTestPageView0;
     PageView mTestPageView1;
@@ -71,9 +78,9 @@
 
         mPaginatedView.setModel(mPaginationModel);
         mTestPageView0 = new PageMosaicView(mContext, 0, mDimensions, mMockBitmapSource,
-                mMockBitmapRecycler);
+                mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
         mTestPageView1 = new PageMosaicView(mContext, 1, mDimensions, mMockBitmapSource,
-                mMockBitmapRecycler);
+                mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
     }
 
     @After
@@ -98,7 +105,7 @@
 
         mPaginationModel.addPage(2, mDimensions);
         PageView testPageView3 = new PageMosaicView(mContext, 2, mDimensions, mMockBitmapSource,
-                mMockBitmapRecycler);
+                mMockBitmapRecycler, mMockPdfLoader, mPdfSelectionModel, mSearchModel);
         mPaginatedView.addView(testPageView3);
 
         mPaginatedView.removeViewAt(1);
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java
new file mode 100644
index 0000000..d726676
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SearchQueryObserverTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class SearchQueryObserverTest {
+
+    private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+
+    @Test
+    public void onChange() {
+        SearchQueryObserver searchQueryObserver = new SearchQueryObserver(mMockPaginatedView);
+        searchQueryObserver.onChange("oldQuery", "newQuery");
+        verify(mMockPaginatedView).clearAllOverlays();
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
new file mode 100644
index 0000000..7ac5b06
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/SelectedMatchValueObserverTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+
+import androidx.pdf.models.Dimensions;
+import androidx.pdf.models.MatchRects;
+import androidx.pdf.widget.ZoomView;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class SelectedMatchValueObserverTest {
+    private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+    private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+    private final PageViewFactory mMockPageViewFactory = mock(PageViewFactory.class);
+    private final ZoomView mMockZoomView = mock(ZoomView.class);
+    private final LayoutHandler mMockLayoutHandler = mock(LayoutHandler.class);
+    private final SelectedMatch mMockOldSelection = mock(SelectedMatch.class);
+    private final SelectedMatch mMockNewSelection = mock(SelectedMatch.class);
+    private final PageViewFactory.PageView mMockOldPageView = mock(PageViewFactory.PageView.class);
+    private final PageViewFactory.PageView mMockNewPageView = mock(PageViewFactory.PageView.class);
+    private final PageMosaicView mMockOldPageMosaicView = mock(PageMosaicView.class);
+    private final PageMosaicView mMockNewPageMosaicView = mock(PageMosaicView.class);
+    private final MatchRects mMockMatchRects = mock(MatchRects.class);
+    private final PdfHighlightOverlay mMockPdfHighlightOverlay = mock(PdfHighlightOverlay.class);
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
+    @Test
+    public void onChange_setOverlay() {
+        when(mMockOldSelection.getPage()).thenReturn(1);
+        when(mMockNewSelection.getPage()).thenReturn(2);
+        when(mMockPaginationModel.getSize()).thenReturn(3);
+        when(mMockPaginatedView.getViewAt(1)).thenReturn(mMockOldPageView);
+        when(mMockPaginatedView.getViewAt(2)).thenReturn(mMockNewPageView);
+        when(mMockPaginationModel.getPageSize(2)).thenReturn(new Dimensions(100, 100));
+        when(mMockOldPageView.getPageView()).thenReturn(mMockOldPageMosaicView);
+        when(mMockPageViewFactory.getOrCreatePageView(2, 2, new Dimensions(100, 100))).thenReturn(
+                mMockNewPageMosaicView);
+        when(mMockOldSelection.getPageMatches()).thenReturn(
+                new MatchRects(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()));
+        when(mMockNewSelection.getPageMatches()).thenReturn(mMockMatchRects);
+        when(mMockNewSelection.getSelected()).thenReturn(1);
+        when(mMockNewSelection.getPage()).thenReturn(2);
+        when(mMockMatchRects.getFirstRect(1)).thenReturn(new Rect(0, 0, 0, 0));
+        when(mMockPaginationModel.getLookAtX(2, 0)).thenReturn(0);
+        when(mMockPaginationModel.getLookAtY(2, 0)).thenReturn(0);
+        when(mMockNewSelection.getOverlay()).thenReturn(mMockPdfHighlightOverlay);
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        displayMetrics.density = 1f;
+        mContext.getResources().getDisplayMetrics().setTo(displayMetrics);
+        PdfViewer.setScreenForTest(mContext);
+
+        SelectedMatchValueObserver selectedMatchValueObserver = new SelectedMatchValueObserver(
+                mMockPaginatedView, mMockPaginationModel, mMockPageViewFactory, mMockZoomView,
+                mMockLayoutHandler, mContext);
+        selectedMatchValueObserver.onChange(mMockOldSelection, mMockNewSelection);
+
+        verify(mMockZoomView).centerAt(0, 0);
+        verify(mMockNewPageMosaicView).setOverlay(mMockPdfHighlightOverlay);
+    }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
new file mode 100644
index 0000000..f9c46a9
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.pdf.viewer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.pdf.data.Range;
+import androidx.pdf.find.FindInFileView;
+import androidx.pdf.util.ObservableValue;
+import androidx.pdf.util.Observables;
+import androidx.pdf.widget.FastScrollView;
+import androidx.pdf.widget.ZoomView;
+import androidx.test.filters.SmallTest;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class ZoomScrollValueObserverTest {
+    private static final ObservableValue<Viewer.ViewState> VIEW_STATE_EXPOSED_VALUE =
+            Observables.newExposedValueWithInitialValue(Viewer.ViewState.NO_VIEW);
+    private static final Rect RECT = new Rect(0, 0, 100, 100);
+    private static final ZoomView.ZoomScroll OLD_POSITION = new ZoomView.ZoomScroll(1.0f, 0, 0,
+            false);
+    private static final Range PAGE_RANGE = new Range(0, 100);
+
+    private final PaginatedView mMockPaginatedView = mock(PaginatedView.class);
+    private final ZoomView mMockZoomView = mock(ZoomView.class);
+    private final PaginationModel mMockPaginationModel = mock(PaginationModel.class);
+    private final LayoutHandler mMockLayoutHandler = mock(LayoutHandler.class);
+    private final FloatingActionButton mMockAnnotationButton = mock(FloatingActionButton.class);
+    private final FindInFileView mMockFindInFileView = mock(FindInFileView.class);
+    private final PageIndicator mMockPageIndicator = mock(PageIndicator.class);
+    private final FastScrollView mMockFastScrollView = mock(FastScrollView.class);
+    private final PageRangeHandler mPageRangeHandler = mock(PageRangeHandler.class);
+
+    private boolean mIsAnnotationIntentResolvable;
+    private ZoomView.ZoomScroll mNewPosition;
+
+
+    @Before
+    public void setUp() {
+        mIsAnnotationIntentResolvable = false;
+        mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, false);
+
+        when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mPageRangeHandler);
+        when(mMockPaginatedView.getPaginationModel()).thenReturn(mMockPaginationModel);
+        when(mMockZoomView.getHeight()).thenReturn(100);
+        when(mPageRangeHandler.computeVisibleRange(0, 1.0f, 100, false)).thenReturn(PAGE_RANGE);
+        when(mMockPageIndicator.setRangeAndZoom(PAGE_RANGE, 1.0f, true)).thenReturn(false);
+        when(mMockZoomView.getStableZoom()).thenReturn(1.0f);
+        when(mMockZoomView.getVisibleAreaInContentCoords()).thenReturn(RECT);
+        when(mMockPaginatedView.createPageViewsForVisiblePageRange()).thenReturn(false);
+        when(mPageRangeHandler.getVisiblePages()).thenReturn(PAGE_RANGE);
+    }
+
+    @Test
+    public void onChange_loadPageAssets_stablePosition() {
+        mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, true);
+
+        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+                mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+                mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+        verify(mMockZoomView).setStableZoom(1.0f);
+        verify(mMockPaginationModel).setViewArea(RECT);
+        verify(mMockPaginatedView).refreshPageRangeInVisibleArea(mNewPosition, 100);
+        verify(mMockPaginatedView).handleGonePages(false);
+        verify(mMockPaginatedView).loadInvisibleNearPageRange(1.0f);
+        verify(mMockPaginatedView).refreshVisiblePages(false, Viewer.ViewState.NO_VIEW, 1.0f);
+        verify(mMockPaginatedView).handleGonePages(true);
+        verify(mMockLayoutHandler).maybeLayoutPages(100);
+    }
+
+    @Test
+    public void onChange_loadPageAssets_stableZoom() {
+        mNewPosition = new ZoomView.ZoomScroll(2.0f, 0, 0, false);
+        when(mMockZoomView.getStableZoom()).thenReturn(2.0f);
+
+        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+                mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+                mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+        verify(mMockPaginatedView).refreshVisibleTiles(false, Viewer.ViewState.NO_VIEW);
+    }
+
+    @Test
+    public void onChange_showFastScrollView() {
+        when(mMockPageIndicator.setRangeAndZoom(PAGE_RANGE, 1.0f, false)).thenReturn(true);
+
+        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+                mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+                mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+        verify(mMockFastScrollView).setVisible();
+    }
+
+    @Test
+    public void onChange_showAnnotationButton() {
+        mIsAnnotationIntentResolvable = true;
+        when(mMockAnnotationButton.getVisibility()).thenReturn(View.GONE);
+        when(mMockFindInFileView.getVisibility()).thenReturn(View.GONE);
+
+        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+                mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+                mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+        verify(mMockAnnotationButton).setVisibility(View.VISIBLE);
+    }
+
+    @Test
+    public void onChange_hideAnnotationButton() {
+        mIsAnnotationIntentResolvable = true;
+        mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 1, false);
+
+        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
+                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
+                mMockFindInFileView, mMockPageIndicator, mMockFastScrollView,
+                mIsAnnotationIntentResolvable, VIEW_STATE_EXPOSED_VALUE);
+        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
+
+        verify(mMockAnnotationButton).setVisibility(View.GONE);
+    }
+
+}
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index c4d6592..0e954eb 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -55,6 +55,7 @@
     }
 
     defaultConfig {
+        compileSdk = 35
         testInstrumentationRunner "androidx.recyclerview.test.TestRunner"
     }
     namespace "androidx.recyclerview"
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
new file mode 100644
index 0000000..1972555
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.recyclerview.widget
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.widget.TextView
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+// TODO: change to VANILLA_ICE_CREAM when it is ready
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(AndroidJUnit4::class)
+class RecyclerViewScrollFrameRateTest {
+    @get:Rule val rule = ActivityTestRule(TestContentViewActivity::class.java)
+
+    @Test
+    fun smoothScrollFrameRateBoost() {
+        // TODO: Remove when VANILLA_ICE_CREAM is ready and the SdkSuppress is modified
+        if (!BuildCompat.isAtLeastV()) {
+            return
+        }
+        val rv = RecyclerView(rule.activity)
+        rule.runOnUiThread {
+            rv.layoutManager =
+                LinearLayoutManager(rule.activity, LinearLayoutManager.VERTICAL, false)
+            rv.adapter =
+                object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+                    override fun onCreateViewHolder(
+                        parent: ViewGroup,
+                        viewType: Int
+                    ): RecyclerView.ViewHolder {
+                        val view = TextView(parent.context)
+                        view.textSize = 40f
+                        view.setTextColor(Color.WHITE)
+                        return object : RecyclerView.ViewHolder(view) {}
+                    }
+
+                    override fun getItemCount(): Int = 10000
+
+                    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+                        val view = holder.itemView as TextView
+                        view.text = "Text $position"
+                        val color = if (position % 2 == 0) Color.BLACK else 0xFF000080.toInt()
+                        view.setBackgroundColor(color)
+                    }
+                }
+            rule.activity.contentView.addView(rv)
+        }
+        runOnDraw(rv, { rv.smoothScrollBy(0, 1000) }) {
+            // First Frame
+            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
+        }
+
+        // Second frame
+        runOnDraw(rv) { assertThat(rv.frameContentVelocity).isGreaterThan(0f) }
+
+        // Third frame
+        runOnDraw(rv) { assertThat(rv.frameContentVelocity).isGreaterThan(0f) }
+    }
+
+    private fun runOnDraw(view: View, setup: () -> Unit = {}, onDraw: () -> Unit) {
+        val latch = CountDownLatch(1)
+        val onDrawListener =
+            ViewTreeObserver.OnDrawListener {
+                latch.countDown()
+                onDraw()
+            }
+        rule.runOnUiThread {
+            view.viewTreeObserver.addOnDrawListener(onDrawListener)
+            setup()
+        }
+        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
+        rule.runOnUiThread { view.viewTreeObserver.removeOnDrawListener(onDrawListener) }
+    }
+}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 80be2f0..9d4ef74 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -64,12 +64,15 @@
 import android.widget.OverScroller;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.DoNotInline;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.core.os.TraceCompat;
 import androidx.core.util.Preconditions;
 import androidx.core.view.AccessibilityDelegateCompat;
@@ -6005,6 +6008,10 @@
                         mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                     }
                 }
+                if (BuildCompat.isAtLeastV()) {
+                    Api35Impl.setFrameContentVelocity(RecyclerView.this,
+                            Math.abs(scroller.getCurrVelocity()));
+                }
             }
 
             SmoothScroller smoothScroller = mLayout.mSmoothScroller;
@@ -14627,4 +14634,16 @@
         }
         return mScrollingChildHelper;
     }
+
+    @RequiresApi(35)
+    private static final class Api35Impl {
+        @DoNotInline
+        public static void setFrameContentVelocity(View view, float velocity) {
+            try {
+                view.setFrameContentVelocity(velocity);
+            } catch (LinkageError e) {
+                // The setFrameContentVelocity method is unavailable on this device.
+            }
+        }
+    }
 }
diff --git a/samples/AndroidXDemos/build.gradle b/samples/AndroidXDemos/build.gradle
index 519d459..048ac9c 100644
--- a/samples/AndroidXDemos/build.gradle
+++ b/samples/AndroidXDemos/build.gradle
@@ -41,6 +41,7 @@
             proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
         }
     }
+    compileSdkVersion 35
     defaultConfig {
         vectorDrawables.useSupportLibrary = true
     }
diff --git a/samples/MediaRoutingDemo/build.gradle b/samples/MediaRoutingDemo/build.gradle
index 6da205a..617f702 100644
--- a/samples/MediaRoutingDemo/build.gradle
+++ b/samples/MediaRoutingDemo/build.gradle
@@ -28,6 +28,7 @@
             proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
         }
     }
+    compileSdk 35
     defaultConfig {
         vectorDrawables.useSupportLibrary = true
     }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
index aed9f13..cb1f13b 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
@@ -44,7 +44,7 @@
 public class AddEditRouteActivity extends AppCompatActivity {
     private static final String EXTRA_ROUTE_ID_KEY = "routeId";
 
-    private SampleDynamicGroupMediaRouteProviderService mService;
+    @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
     private ServiceConnection mConnection;
     private RoutesManager mRoutesManager;
     private RouteItem mRouteItem;
@@ -163,7 +163,9 @@
         saveButton.setOnClickListener(
                 view -> {
                     mRoutesManager.addRoute(mRouteItem);
-                    mService.reloadRoutes();
+                    if (mService != null) {
+                        mService.reloadRoutes();
+                    }
                     finish();
                 });
     }
@@ -203,7 +205,7 @@
         }
 
         @Override
-        public void onServiceDisconnected(ComponentName arg0) {
+        public void onServiceDisconnected(ComponentName unusedComponentName) {
             mService = null;
         }
     }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
index 65e89dd..97dd936 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
@@ -648,6 +648,9 @@
         @Override
         public void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) {
             Log.d(TAG, "onRouteChanged: route=" + route);
+            if (route.isSelected()) {
+                updateRouteDescription();
+            }
         }
 
         @Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
index 2db33b8..1285855 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.view.View;
@@ -43,6 +44,7 @@
 import com.example.androidx.mediarouting.RoutesManager;
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutingActivity;
 import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
+import com.example.androidx.mediarouting.services.SampleMediaRouteProviderService;
 import com.example.androidx.mediarouting.ui.RoutesAdapter;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 
@@ -52,20 +54,20 @@
  * SampleDynamicGroupMediaRouteProviderService}.
  */
 public final class SettingsActivity extends AppCompatActivity {
+    private final ProviderServiceConnection mConnection = new ProviderServiceConnection();
+    private PackageManager mPackageManager;
     private MediaRouter mMediaRouter;
     private RoutesManager mRoutesManager;
     private RoutesAdapter mRoutesAdapter;
-    private SampleDynamicGroupMediaRouteProviderService mService;
-    private ServiceConnection mConnection;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_settings);
 
+        mPackageManager = getPackageManager();
         mMediaRouter = MediaRouter.getInstance(this);
         mRoutesManager = RoutesManager.getInstance(getApplicationContext());
-        mConnection = new ProviderServiceConnection();
 
         setUpViews();
 
@@ -86,7 +88,11 @@
                                 android.R.string.ok,
                                 (dialogInterface, i) -> {
                                     mRoutesManager.deleteRouteWithId(routeId);
-                                    mService.reloadRoutes();
+                                    SampleDynamicGroupMediaRouteProviderService providerService =
+                                            mConnection.mService;
+                                    if (providerService != null) {
+                                        providerService.reloadRoutes();
+                                    }
                                     mRoutesAdapter.updateRoutes(
                                             mRoutesManager.getRouteItems());
                                 })
@@ -111,10 +117,7 @@
     @Override
     protected void onStart() {
         super.onStart();
-        // Bind to SampleDynamicGroupMediaRouteProviderService
-        Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
-        intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
-        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+        bindToDynamicProviderService();
     }
 
     @Override
@@ -125,13 +128,20 @@
 
     @Override
     protected void onStop() {
+        try {
+            unbindService(mConnection);
+        } catch (RuntimeException e) {
+            // This happens when the provider is disabled, but there's no way of preventing this
+            // completely so we just ignore the exception.
+        }
         super.onStop();
-        unbindService(mConnection);
     }
 
     private void setUpViews() {
         setUpDynamicGroupsEnabledSwitch();
         setUpTransferToLocalSwitch();
+        setUpSimpleProviderEnabledSwitch();
+        setUpDynamicProviderEnabledSwitch();
         setUpDialogTypeDropDownList();
         setUpNewRouteButton();
         setupSystemRoutesButton();
@@ -141,9 +151,13 @@
         Switch dynamicRoutingEnabled = findViewById(R.id.dynamic_routing_switch);
         dynamicRoutingEnabled.setChecked(mRoutesManager.isDynamicRoutingEnabled());
         dynamicRoutingEnabled.setOnCheckedChangeListener(
-                (compoundButton, b) -> {
-                    mRoutesManager.setDynamicRoutingEnabled(b);
-                    mService.reloadDynamicRoutesEnabled();
+                (compoundButton, enabled) -> {
+                    mRoutesManager.setDynamicRoutingEnabled(enabled);
+                    SampleDynamicGroupMediaRouteProviderService providerService =
+                            mConnection.mService;
+                    if (providerService != null) {
+                        providerService.reloadDynamicRoutesEnabled();
+                    }
                 });
     }
 
@@ -151,14 +165,56 @@
         Switch showThisPhoneSwitch = findViewById(R.id.show_this_phone_switch);
         showThisPhoneSwitch.setChecked(mMediaRouter.getRouterParams().isTransferToLocalEnabled());
         showThisPhoneSwitch.setOnCheckedChangeListener(
-                (compoundButton, b) -> {
+                (compoundButton, enabled) -> {
                     MediaRouterParams.Builder builder =
                             new MediaRouterParams.Builder(mMediaRouter.getRouterParams());
-                    builder.setTransferToLocalEnabled(b);
+                    builder.setTransferToLocalEnabled(enabled);
                     mMediaRouter.setRouterParams(builder.build());
                 });
     }
 
+    private void setUpSimpleProviderEnabledSwitch() {
+        Switch simpleProviderEnabledSwitch = findViewById(R.id.enable_simple_provider_switch);
+        ComponentName simpleProviderComponentName =
+                new ComponentName(/* context= */ this, SampleMediaRouteProviderService.class);
+        simpleProviderEnabledSwitch.setChecked(
+                mPackageManager.getComponentEnabledSetting(simpleProviderComponentName)
+                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+        simpleProviderEnabledSwitch.setOnCheckedChangeListener(
+                (compoundButton, enabled) -> {
+                    mPackageManager
+                            .setComponentEnabledSetting(
+                                    simpleProviderComponentName,
+                                    enabled
+                                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                                    /* flags= */ PackageManager.DONT_KILL_APP);
+                });
+    }
+
+    private void setUpDynamicProviderEnabledSwitch() {
+        Switch dynamicProviderEnabledSwitch = findViewById(R.id.enable_dynamic_provider_switch);
+        ComponentName dynamicProviderComponentName =
+                new ComponentName(
+                        /* context= */ this, SampleDynamicGroupMediaRouteProviderService.class);
+        dynamicProviderEnabledSwitch.setChecked(
+                mPackageManager.getComponentEnabledSetting(dynamicProviderComponentName)
+                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+        dynamicProviderEnabledSwitch.setOnCheckedChangeListener(
+                (compoundButton, enabled) -> {
+                    mPackageManager
+                            .setComponentEnabledSetting(
+                                    dynamicProviderComponentName,
+                                    enabled
+                                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                                    /* flags= */ PackageManager.DONT_KILL_APP);
+                    if (enabled) {
+                        bindToDynamicProviderService();
+                    }
+                });
+    }
+
     private void setUpDialogTypeDropDownList() {
         Spinner spinner = findViewById(R.id.dialog_spinner);
         spinner.setOnItemSelectedListener(
@@ -204,13 +260,22 @@
         showSystemRoutesButton.setOnClickListener(v -> SystemRoutingActivity.launch(this));
     }
 
-    private class ProviderServiceConnection implements ServiceConnection {
+    private void bindToDynamicProviderService() {
+        Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
+        intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    private static class ProviderServiceConnection implements ServiceConnection {
+
+        @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
 
         @Override
         public void onServiceConnected(ComponentName className, IBinder service) {
             SampleDynamicGroupMediaRouteProviderService.LocalBinder binder =
                     (SampleDynamicGroupMediaRouteProviderService.LocalBinder) service;
             mService = binder.getService();
+            mService.reloadRoutes();
         }
 
         @Override
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
index c92ced8..0f331cd 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
@@ -19,71 +19,66 @@
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
 
 import java.util.Objects;
 
-import javax.annotation.Nullable;
-
-/**
- * An abstract model that holds information about routes from different sources.
- *
- * Can represent media routers' routes, bluetooth routes, or audio routes.
- */
+/** Holds information about a system route. */
 public final class SystemRouteItem implements SystemRoutesAdapterItem {
 
-    @NonNull
-    private final String mId;
+    /**
+     * Describes the support of a route for selection.
+     *
+     * <p>We understand by selection the action that makes a specific route the active route. Note
+     * that this terminology may not match the terminology used by the underlying {@link
+     * SystemRoutesSource}.
+     */
+    public enum SelectionSupportState {
+        /** The underlying route source doesn't support selection. */
+        UNSUPPORTED,
+        /**
+         * The corresponding route is already selected, but can be reselected.
+         *
+         * <p>Selecting an already selected route (reselection) can change the metadata of the route
+         * source. For example, reselecting a MediaRouter2 route can alter the transfer reason.
+         */
+        RESELECTABLE,
+        /** The route is available for selection. */
+        SELECTABLE
+    }
 
-    @NonNull
-    private final String mName;
+    /** The {@link SystemRoutesSource#getSourceId()} of the source that created this item. */
+    @NonNull public final String mSourceId;
 
-    @Nullable
-    private final String mAddress;
+    /** An id that uniquely identifies this route item within the source. */
+    @NonNull public final String mId;
 
-    @Nullable
-    private final String mDescription;
+    @NonNull public final String mName;
+
+    @Nullable public final String mAddress;
+
+    @Nullable public final String mDescription;
+
+    @Nullable public final String mSuitabilityStatus;
+
+    @Nullable public final Boolean mTransferInitiatedBySelf;
+
+    @Nullable public final String mTransferReason;
+
+    @NonNull public final SelectionSupportState mSelectionSupportState;
 
     private SystemRouteItem(@NonNull Builder builder) {
-        Objects.requireNonNull(builder.mId);
-        Objects.requireNonNull(builder.mName);
-
-        mId = builder.mId;
-        mName = builder.mName;
-
+        mSourceId = Objects.requireNonNull(builder.mSourceId);
+        mId = Objects.requireNonNull(builder.mId);
+        mName = Objects.requireNonNull(builder.mName);
         mAddress = builder.mAddress;
         mDescription = builder.mDescription;
-    }
-
-    /**
-     * Returns a unique identifier of a route.
-     */
-    @NonNull
-    public String getId() {
-        return mId;
-    }
-
-    /**
-     * Returns a human-readable name of the route.
-     */
-    @NonNull
-    public String getName() {
-        return mName;
-    }
-
-    /**
-     * Returns address if the route is a Bluetooth route and {@code null} otherwise.
-     */
-    @Nullable
-    public String getAddress() {
-        return mAddress;
-    }
-
-    /**
-     * Returns a route description or {@code null} if empty.
-     */
-    @Nullable
-    public String getDescription() {
-        return mDescription;
+        mSuitabilityStatus = builder.mSuitabilityStatus;
+        mTransferInitiatedBySelf = builder.mTransferInitiatedBySelf;
+        mTransferReason = builder.mTransferReason;
+        mSelectionSupportState = builder.mSelectionSupportState;
     }
 
     @Override
@@ -91,14 +86,29 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         SystemRouteItem that = (SystemRouteItem) o;
-        return mId.equals(that.mId) && mName.equals(that.mName)
-                && Objects.equals(mAddress, that.mAddress) && Objects.equals(
-                mDescription, that.mDescription);
+        return mSourceId.equals(that.mSourceId)
+                && mId.equals(that.mId)
+                && mName.equals(that.mName)
+                && Objects.equals(mAddress, that.mAddress)
+                && Objects.equals(mDescription, that.mDescription)
+                && Objects.equals(mSuitabilityStatus, that.mSuitabilityStatus)
+                && Objects.equals(mTransferInitiatedBySelf, that.mTransferInitiatedBySelf)
+                && Objects.equals(mTransferReason, that.mTransferReason)
+                && mSelectionSupportState.equals(that.mSelectionSupportState);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mName, mAddress, mDescription);
+        return Objects.hash(
+                mSourceId,
+                mId,
+                mName,
+                mAddress,
+                mDescription,
+                mSuitabilityStatus,
+                mTransferInitiatedBySelf,
+                mTransferReason,
+                mSelectionSupportState);
     }
 
     /**
@@ -106,20 +116,26 @@
      */
     public static final class Builder {
 
-        @NonNull
-        private final String mId;
+        @NonNull private String mSourceId;
+        @NonNull private final String mId;
+        @NonNull private String mName;
+        @Nullable private String mAddress;
+        @Nullable private String mDescription;
+        @Nullable private String mSuitabilityStatus;
+        @Nullable private Boolean mTransferInitiatedBySelf;
+        @Nullable private String mTransferReason;
+        @NonNull public SelectionSupportState mSelectionSupportState;
 
-        @NonNull
-        private String mName;
-
-        @Nullable
-        private String mAddress;
-
-        @Nullable
-        private String mDescription;
-
-        public Builder(@NonNull String id) {
+        /**
+         * Creates a builder with the mandatory properties.
+         *
+         * @param sourceId See {@link SystemRouteItem#mSourceId}.
+         * @param id See {@link SystemRouteItem#mId}.
+         */
+        public Builder(@NonNull String sourceId, @NonNull String id) {
+            mSourceId = sourceId;
             mId = id;
+            mSelectionSupportState = SelectionSupportState.UNSUPPORTED;
         }
 
         /**
@@ -154,6 +170,43 @@
         }
 
         /**
+         * Sets a human-readable string describing the transfer suitability of the route, or null if
+         * not applicable.
+         */
+        @NonNull
+        public Builder setSuitabilityStatus(@Nullable String suitabilityStatus) {
+            mSuitabilityStatus = suitabilityStatus;
+            return this;
+        }
+
+        /**
+         * Sets whether the corresponding route's selection is the result of an action of this app,
+         * or null if not applicable.
+         */
+        @NonNull
+        public Builder setTransferInitiatedBySelf(@Nullable Boolean transferInitiatedBySelf) {
+            mTransferInitiatedBySelf = transferInitiatedBySelf;
+            return this;
+        }
+
+        /**
+         * Sets a human-readable string describing the transfer reason, or null if not applicable.
+         */
+        @NonNull
+        public Builder setTransferReason(@Nullable String transferReason) {
+            mTransferReason = transferReason;
+            return this;
+        }
+
+        /** Sets the {@link SelectionSupportState} for the corresponding route. */
+        @NonNull
+        public Builder setSelectionSupportState(
+                @NonNull SelectionSupportState selectionSupportState) {
+            mSelectionSupportState = Objects.requireNonNull(selectionSupportState);
+            return this;
+        }
+
+        /**
          * Builds {@link SystemRouteItem}.
          */
         @NonNull
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
index aba49a1..764c050 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
@@ -16,14 +16,21 @@
 
 package com.example.androidx.mediarouting.activities.systemrouting;
 
+import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.SELECTABLE;
+import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.UNSUPPORTED;
+
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatButton;
 import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.util.Consumer;
 import androidx.recyclerview.widget.AsyncListDiffer;
 import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.RecyclerView;
@@ -32,17 +39,19 @@
 
 import java.util.List;
 
-/**
- * @link RecyclerView.Adapter} for showing system route sources and the routes discovered by each
- * source.
- */
-class SystemRoutesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+/** {@link RecyclerView.Adapter} for showing system route sources and their corresponding routes. */
+/* package */ class SystemRoutesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 
     private static final int VIEW_TYPE_HEADER = 0;
     private static final int VIEW_TYPE_ITEM = 1;
 
     private final AsyncListDiffer<SystemRoutesAdapterItem> mListDiffer =
             new AsyncListDiffer<>(this, new ItemCallback());
+    private final Consumer<SystemRouteItem> mRouteItemClickedListener;
+
+    /* package */ SystemRoutesAdapter(Consumer<SystemRouteItem> routeItemClickListener) {
+        mRouteItemClickedListener = routeItemClickListener;
+    }
 
     public void setItems(@NonNull List<SystemRoutesAdapterItem> newItems) {
         mListDiffer.submitList(newItems);
@@ -108,12 +117,16 @@
         }
     }
 
-    static class ItemViewHolder extends RecyclerView.ViewHolder {
+    private class ItemViewHolder extends RecyclerView.ViewHolder {
 
         private final AppCompatTextView mRouteNameTextView;
         private final AppCompatTextView mRouteIdTextView;
         private final AppCompatTextView mRouteAddressTextView;
         private final AppCompatTextView mRouteDescriptionTextView;
+        private final AppCompatTextView mSuitabilityStatusTextView;
+        private final AppCompatTextView mTransferInitiatedBySelfTextView;
+        private final AppCompatTextView mTransferReasonTextView;
+        private final AppCompatButton mSelectionButton;
 
         ItemViewHolder(@NonNull View itemView) {
             super(itemView);
@@ -122,20 +135,41 @@
             mRouteIdTextView = itemView.findViewById(R.id.route_id);
             mRouteAddressTextView = itemView.findViewById(R.id.route_address);
             mRouteDescriptionTextView = itemView.findViewById(R.id.route_description);
+            mSuitabilityStatusTextView = itemView.findViewById(R.id.route_suitability_status);
+            mTransferInitiatedBySelfTextView =
+                    itemView.findViewById(R.id.route_transfer_initiated_by_self);
+            mTransferReasonTextView = itemView.findViewById(R.id.route_transfer_reason);
+            mSelectionButton = itemView.findViewById(R.id.route_selection_button);
         }
 
         void bind(SystemRouteItem systemRouteItem) {
-            mRouteNameTextView.setText(systemRouteItem.getName());
-            mRouteIdTextView.setText(systemRouteItem.getId());
+            mRouteNameTextView.setText(systemRouteItem.mName);
+            mRouteIdTextView.setText(systemRouteItem.mId);
+            setTextOrHide(mRouteAddressTextView, systemRouteItem.mAddress);
+            setTextOrHide(mRouteDescriptionTextView, systemRouteItem.mDescription);
+            setTextOrHide(mSuitabilityStatusTextView, systemRouteItem.mSuitabilityStatus);
+            String initiatedBySelfText =
+                    systemRouteItem.mTransferInitiatedBySelf != null
+                            ? "self-initiated: " + systemRouteItem.mTransferInitiatedBySelf
+                            : null;
+            setTextOrHide(mTransferInitiatedBySelfTextView, initiatedBySelfText);
+            String transferReasonText =
+                    systemRouteItem.mTransferReason != null
+                            ? "transfer reason: " + systemRouteItem.mTransferReason
+                            : null;
+            setTextOrHide(mTransferReasonTextView, transferReasonText);
 
-            showViewIfNotNull(mRouteAddressTextView, systemRouteItem.getAddress());
-            if (systemRouteItem.getAddress() != null) {
-                mRouteAddressTextView.setText(systemRouteItem.getAddress());
-            }
-
-            showViewIfNotNull(mRouteDescriptionTextView, systemRouteItem.getDescription());
-            if (systemRouteItem.getDescription() != null) {
-                mRouteDescriptionTextView.setText(systemRouteItem.getDescription());
+            if (systemRouteItem.mSelectionSupportState == UNSUPPORTED) {
+                mSelectionButton.setVisibility(View.GONE);
+            } else {
+                mSelectionButton.setVisibility(View.VISIBLE);
+                String text =
+                        systemRouteItem.mSelectionSupportState == SELECTABLE
+                                ? "Select"
+                                : "Reselect";
+                mSelectionButton.setText(text);
+                mSelectionButton.setOnClickListener(
+                        view -> mRouteItemClickedListener.accept(systemRouteItem));
             }
         }
     }
@@ -145,8 +179,7 @@
         public boolean areItemsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
                 @NonNull SystemRoutesAdapterItem newItem) {
             if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
-                return ((SystemRouteItem) oldItem).getId().equals(
-                        ((SystemRouteItem) newItem).getId());
+                return ((SystemRouteItem) oldItem).mId.equals(((SystemRouteItem) newItem).mId);
             } else if (oldItem instanceof SystemRoutesSourceItem
                     && newItem instanceof SystemRoutesSourceItem) {
                 return oldItem.equals(newItem);
@@ -155,25 +188,21 @@
             }
         }
 
+        @SuppressLint("DiffUtilEquals")
         @Override
-        public boolean areContentsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
+        public boolean areContentsTheSame(
+                @NonNull SystemRoutesAdapterItem oldItem,
                 @NonNull SystemRoutesAdapterItem newItem) {
-            if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
-                return oldItem.equals(newItem);
-            } else if (oldItem instanceof SystemRoutesSourceItem
-                    && newItem instanceof SystemRoutesSourceItem) {
-                return oldItem.equals(newItem);
-            } else {
-                return false;
-            }
+            return oldItem.equals(newItem);
         }
     }
 
-    private static <T, V extends View> void showViewIfNotNull(@NonNull V view, @Nullable T obj) {
-        if (obj == null) {
+    private static void setTextOrHide(@NonNull TextView view, @Nullable String text) {
+        if (text == null) {
             view.setVisibility(View.GONE);
         } else {
             view.setVisibility(View.VISIBLE);
+            view.setText(text);
         }
     }
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
index 73d57fe..6f18b57 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
@@ -44,7 +44,9 @@
 import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Shows available system routes gathered from different sources.
@@ -54,15 +56,11 @@
     private static final int REQUEST_CODE_BLUETOOTH_CONNECT = 4199;
 
     @NonNull
-    private final SystemRoutesAdapter mSystemRoutesAdapter = new SystemRoutesAdapter();
-    @NonNull
-    private final List<SystemRoutesSource> mSystemRoutesSources = new ArrayList<>();
-    @NonNull
-    private final SystemRoutesSourceCallback mSystemRoutesSourceCallback =
-            new SystemRoutesSourceCallback();
+    private final SystemRoutesAdapter mSystemRoutesAdapter =
+            new SystemRoutesAdapter(this::onRouteItemClicked);
 
-    @NonNull
-    private SwipeRefreshLayout mSwipeRefreshLayout;
+    @NonNull private final Map<String, SystemRoutesSource> mSystemRoutesSources = new HashMap<>();
+    @NonNull private SwipeRefreshLayout mSwipeRefreshLayout;
 
     /**
      * Creates and launches an intent to start current activity.
@@ -95,7 +93,7 @@
 
     @Override
     protected void onDestroy() {
-        for (SystemRoutesSource source: mSystemRoutesSources) {
+        for (SystemRoutesSource source : mSystemRoutesSources.values()) {
             source.stop();
         }
 
@@ -119,7 +117,7 @@
 
     private void refreshSystemRoutesList() {
         List<SystemRoutesAdapterItem> systemRoutesSourceItems = new ArrayList<>();
-        for (SystemRoutesSource source : mSystemRoutesSources) {
+        for (SystemRoutesSource source : mSystemRoutesSources.values()) {
             systemRoutesSourceItems.add(source.getSourceItem());
             systemRoutesSourceItems.addAll(source.fetchSourceRouteItems());
         }
@@ -127,6 +125,20 @@
         mSwipeRefreshLayout.setRefreshing(false);
     }
 
+    private void onRouteItemClicked(SystemRouteItem item) {
+        SystemRoutesSource systemRoutesSource = mSystemRoutesSources.get(item.mSourceId);
+        if (systemRoutesSource == null) {
+            throw new IllegalStateException("Couldn't find source with id: " + item.mSourceId);
+        }
+        if (!systemRoutesSource.select(item)) {
+            Toast.makeText(
+                            /* context= */ this,
+                            "Something went wrong with route selection",
+                            Toast.LENGTH_LONG)
+                    .show();
+        }
+    }
+
     private boolean hasBluetoothPermission() {
         return ContextCompat.checkSelfPermission(/* context= */ this, BLUETOOTH_CONNECT)
                 == PERMISSION_GRANTED
@@ -150,40 +162,28 @@
     }
 
     private void initializeSystemRoutesSources() {
-        mSystemRoutesSources.clear();
-
-        mSystemRoutesSources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
+        ArrayList<SystemRoutesSource> sources = new ArrayList<>();
+        sources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-            mSystemRoutesSources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
+            sources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
         }
 
-        mSystemRoutesSources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
+        sources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
 
         if (hasBluetoothPermission()) {
-            mSystemRoutesSources.add(
-                    BluetoothManagerSystemRoutesSource.create(/* context= */ this));
+            sources.add(BluetoothManagerSystemRoutesSource.create(/* context= */ this));
         }
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            mSystemRoutesSources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
+            sources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
         }
 
-        for (SystemRoutesSource source: mSystemRoutesSources) {
-            source.setOnRoutesChangedListener(mSystemRoutesSourceCallback);
+        mSystemRoutesSources.clear();
+        for (SystemRoutesSource source : sources) {
+            source.setOnRoutesChangedListener(this::refreshSystemRoutesList);
+            mSystemRoutesSources.put(source.getSourceId(), source);
             source.start();
         }
     }
-
-    private class SystemRoutesSourceCallback implements SystemRoutesSource.OnRoutesChangedListener {
-        @Override
-        public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
-            refreshSystemRoutesList();
-        }
-
-        @Override
-        public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
-            refreshSystemRoutesList();
-        }
-    }
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
index 7e553ed..d7763d0 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
@@ -36,19 +36,43 @@
     private final MediaRouter mMediaRouter;
 
     @NonNull
-    private final MediaRouter.Callback mMediaRouterCallback = new MediaRouter.Callback() {
-        @Override
-        public void onRouteAdded(@NonNull MediaRouter router,
-                @NonNull MediaRouter.RouteInfo route) {
-            mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
-        }
+    private final MediaRouter.Callback mMediaRouterCallback =
+            new MediaRouter.Callback() {
+                @Override
+                public void onRouteAdded(
+                        @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+                    mOnRoutesChangedListener.run();
+                }
 
-        @Override
-        public void onRouteRemoved(@NonNull MediaRouter router,
-                @NonNull MediaRouter.RouteInfo route) {
-            mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
-        }
-    };
+                @Override
+                public void onRouteRemoved(
+                        @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteSelected(
+                        @NonNull MediaRouter router,
+                        @NonNull MediaRouter.RouteInfo selectedRoute,
+                        int reason,
+                        @NonNull MediaRouter.RouteInfo requestedRoute) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteUnselected(
+                        @NonNull MediaRouter router,
+                        @NonNull MediaRouter.RouteInfo route,
+                        int reason) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteChanged(
+                        @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+                    mOnRoutesChangedListener.run();
+                }
+            };
 
     /** Returns a new instance. */
     @NonNull
@@ -98,11 +122,27 @@
         return out;
     }
 
-    @NonNull
-    private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
-        SystemRouteItem.Builder builder = new SystemRouteItem.Builder(routeInfo.getId())
-                .setName(routeInfo.getName());
+    @Override
+    public boolean select(@NonNull SystemRouteItem item) {
+        for (MediaRouter.RouteInfo routeInfo : mMediaRouter.getRoutes()) {
+            if (routeInfo.getId().equals(item.mId)) {
+                routeInfo.select();
+                return true;
+            }
+        }
+        return false;
+    }
 
+    @NonNull
+    private SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
+        SystemRouteItem.Builder builder =
+                new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
+                        .setName(routeInfo.getName());
+
+        builder.setSelectionSupportState(
+                routeInfo.isSelected()
+                        ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+                        : SystemRouteItem.SelectionSupportState.SELECTABLE);
         String description = routeInfo.getDescription();
         if (description != null) {
             builder.setDescription(description);
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
index 096ebfd..9e7b67a 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
@@ -39,21 +39,18 @@
     private final AudioManager mAudioManager;
 
     @NonNull
-    private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallback() {
-        @Override
-        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
-            for (AudioDeviceInfo audioDeviceInfo: addedDevices) {
-                mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(audioDeviceInfo));
-            }
-        }
+    private final AudioDeviceCallback mAudioDeviceCallback =
+            new AudioDeviceCallback() {
+                @Override
+                public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+                    mOnRoutesChangedListener.run();
+                }
 
-        @Override
-        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
-            for (AudioDeviceInfo audioDeviceInfo: removedDevices) {
-                mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(audioDeviceInfo));
-            }
-        }
-    };
+                @Override
+                public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
+                    mOnRoutesChangedListener.run();
+                }
+            };
 
     /** Returns a new instance. */
     @NonNull
@@ -95,11 +92,16 @@
         return out;
     }
 
+    @Override
+    public boolean select(@NonNull SystemRouteItem item) {
+        throw new UnsupportedOperationException();
+    }
+
     @NonNull
-    private static SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
-        SystemRouteItem.Builder builder = new SystemRouteItem.Builder(
-                String.valueOf(audioDeviceInfo.getId()))
-                .setName(audioDeviceInfo.getProductName().toString());
+    private SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
+        SystemRouteItem.Builder builder =
+                new SystemRouteItem.Builder(getSourceId(), String.valueOf(audioDeviceInfo.getId()))
+                        .setName(audioDeviceInfo.getProductName().toString());
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
             builder.setAddress(Api28Impl.getAddress(audioDeviceInfo));
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
index 96fab5b..7bc34d7 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
@@ -23,7 +23,6 @@
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -31,7 +30,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresPermission;
-import androidx.core.content.IntentCompat;
 
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
@@ -104,10 +102,15 @@
         return out;
     }
 
+    @Override
+    public boolean select(@NonNull SystemRouteItem item) {
+        throw new UnsupportedOperationException();
+    }
+
     @NonNull
     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
-    private static SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
-        return new SystemRouteItem.Builder(/* id= */ device.getAddress())
+    private SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
+        return new SystemRouteItem.Builder(getSourceId(), /* id= */ device.getAddress())
                 .setName(device.getName())
                 .setAddress(device.getAddress())
                 .build();
@@ -117,27 +120,14 @@
         @Override
         @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
         public void onReceive(Context context, Intent intent) {
-            BluetoothDevice device = IntentCompat.getParcelableExtra(intent,
-                    BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
-
             switch (intent.getAction()) {
                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
-                    handleConnectionStateChanged(intent, device);
+                    mOnRoutesChangedListener.run();
                     break;
-            }
-        }
-
-        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
-        private void handleConnectionStateChanged(Intent intent,
-                BluetoothDevice device) {
-            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-            if (state == BluetoothProfile.STATE_CONNECTED) {
-                mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(device));
-            } else if (state == BluetoothProfile.STATE_DISCONNECTING
-                    || state == BluetoothProfile.STATE_DISCONNECTED) {
-                mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(device));
+                default:
+                    // Do nothing.
             }
         }
     }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
index 5785679..1c85f85 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
@@ -16,59 +16,61 @@
 
 package com.example.androidx.mediarouting.activities.systemrouting.source;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2;
 import android.media.RouteDiscoveryPreference;
+import android.media.RoutingSessionInfo;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 /** Implements {@link SystemRoutesSource} using {@link MediaRouter2}. */
 @RequiresApi(Build.VERSION_CODES.R)
 public final class MediaRouter2SystemRoutesSource extends SystemRoutesSource {
 
-    @NonNull
-    private Context mContext;
-    @NonNull
-    private MediaRouter2 mMediaRouter2;
+    @NonNull private final Context mContext;
+    @NonNull private final MediaRouter2 mMediaRouter2;
+    @Nullable private final Method mSuitabilityStatusMethod;
+    @Nullable private final Method mWasTransferInitiatedBySelfMethod;
+    @Nullable private final Method mTransferReasonMethod;
+    @NonNull private final ArrayList<SystemRouteItem> mRouteItems = new ArrayList<>();
 
     @NonNull
-    private final Map<String, MediaRoute2Info> mLastKnownRoutes = new HashMap<>();
-    @NonNull
-    private final MediaRouter2.RouteCallback mRouteCallback = new MediaRouter2.RouteCallback() {
-        @Override
-        public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
-            super.onRoutesUpdated(routes);
-
-            Map<String, MediaRoute2Info> routesLookup = new HashMap<>();
-            for (MediaRoute2Info route: routes) {
-                if (!mLastKnownRoutes.containsKey(route.getId())) {
-                    mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
+    private final MediaRouter2.RouteCallback mRouteCallback =
+            new MediaRouter2.RouteCallback() {
+                @Override
+                public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
+                    populateRouteItems(routes);
                 }
-                routesLookup.put(route.getId(), route);
-            }
+            };
 
-            for (MediaRoute2Info route: mLastKnownRoutes.values()) {
-                if (!routesLookup.containsKey(route.getId())) {
-                    mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
+    @NonNull
+    private final MediaRouter2.ControllerCallback mControllerCallback =
+            new MediaRouter2.ControllerCallback() {
+                @Override
+                public void onControllerUpdated(
+                        @NonNull MediaRouter2.RoutingController unusedController) {
+                    populateRouteItems(mMediaRouter2.getRoutes());
                 }
-            }
-
-            mLastKnownRoutes.clear();
-            mLastKnownRoutes.putAll(routesLookup);
-        }
-    };
+            };
 
     /** Returns a new instance. */
     @NonNull
@@ -81,22 +83,45 @@
             @NonNull MediaRouter2 mediaRouter2) {
         mContext = context;
         mMediaRouter2 = mediaRouter2;
+
+        Method suitabilityStatusMethod = null;
+        Method wasTransferInitiatedBySelfMethod = null;
+        Method transferReasonMethod = null;
+        // TODO: b/336510942 - Remove reflection once these APIs are available in
+        // androidx-platform-dev.
+        try {
+            suitabilityStatusMethod =
+                    MediaRoute2Info.class.getDeclaredMethod("getSuitabilityStatus");
+            wasTransferInitiatedBySelfMethod =
+                    MediaRouter2.RoutingController.class.getDeclaredMethod(
+                            "wasTransferInitiatedBySelf");
+            transferReasonMethod = RoutingSessionInfo.class.getDeclaredMethod("getTransferReason");
+        } catch (NoSuchMethodException | IllegalAccessError e) {
+        }
+        mSuitabilityStatusMethod = suitabilityStatusMethod;
+        mWasTransferInitiatedBySelfMethod = wasTransferInitiatedBySelfMethod;
+        mTransferReasonMethod = transferReasonMethod;
     }
 
     @Override
     public void start() {
         RouteDiscoveryPreference routeDiscoveryPreference =
                 new RouteDiscoveryPreference.Builder(
-                        /* preferredFeatures= */ Collections.emptyList(),
-                        /* activeScan= */ false)
+                                /* preferredFeatures= */ Arrays.asList(
+                                        MediaRoute2Info.FEATURE_LIVE_AUDIO,
+                                        MediaRoute2Info.FEATURE_LIVE_VIDEO),
+                                /* activeScan= */ false)
                         .build();
 
-        mMediaRouter2.registerRouteCallback(mContext.getMainExecutor(),
-                mRouteCallback, routeDiscoveryPreference);
+        Executor mainExecutor = mContext.getMainExecutor();
+        mMediaRouter2.registerRouteCallback(mainExecutor, mRouteCallback, routeDiscoveryPreference);
+        mMediaRouter2.registerControllerCallback(mainExecutor, mControllerCallback);
+        populateRouteItems(mMediaRouter2.getRoutes());
     }
 
     @Override
     public void stop() {
+        mMediaRouter2.unregisterControllerCallback(mControllerCallback);
         mMediaRouter2.unregisterRouteCallback(mRouteCallback);
     }
 
@@ -109,28 +134,141 @@
     @NonNull
     @Override
     public List<SystemRouteItem> fetchSourceRouteItems() {
-        List<SystemRouteItem> out = new ArrayList<>();
+        return mRouteItems;
+    }
 
-        for (MediaRoute2Info routeInfo : mMediaRouter2.getRoutes()) {
-            if (!routeInfo.isSystemRoute()) {
-                continue;
-            }
-
-            if (!mLastKnownRoutes.containsKey(routeInfo.getId())) {
-                mLastKnownRoutes.put(routeInfo.getId(), routeInfo);
-            }
-
-            out.add(createRouteItemFor(routeInfo));
+    @Override
+    public boolean select(@NonNull SystemRouteItem item) {
+        Optional<MediaRoute2Info> route =
+                mMediaRouter2.getRoutes().stream()
+                        .filter(it -> it.getId().equals(item.mId))
+                        .findFirst();
+        if (!route.isPresent()) {
+            return false;
+        } else {
+            mMediaRouter2.transferTo(route.get());
+            return true;
         }
+    }
 
-        return out;
+    // BanUncheckedReflection: See b/336510942 for details on why reflection is needed.
+    // NewApi: We don't need to check the API level because the transfer reason method is only
+    // available on API 35, which is greater than API 34, where getRoutingSessionInfo was added.
+    @SuppressLint({"BanUncheckedReflection", "NewApi"})
+    private void populateRouteItems(List<MediaRoute2Info> routes) {
+        MediaRouter2.RoutingController systemController = mMediaRouter2.getSystemController();
+        Set<String> selectedRoutesIds =
+                systemController.getSelectedRoutes().stream()
+                        .map(MediaRoute2Info::getId)
+                        .collect(Collectors.toSet());
+        Boolean selectionInitiatedBySelf = null;
+        Integer sessionTransferReason = null;
+        try {
+            if (mSuitabilityStatusMethod != null) {
+                selectionInitiatedBySelf =
+                        (Boolean) mWasTransferInitiatedBySelfMethod.invoke(systemController);
+            }
+            if (mTransferReasonMethod != null) {
+                sessionTransferReason =
+                        (Integer)
+                                mTransferReasonMethod.invoke(
+                                        Api34Impl.getRoutingSessionInfo(systemController));
+            }
+        } catch (IllegalAccessException | InvocationTargetException e) {
+        }
+        // We need to filter out non-system routes, which might be reported as a result of other
+        // callbacks with non-system route features being registered in the router.
+        List<MediaRoute2Info> systemRoutes =
+                routes.stream().filter(MediaRoute2Info::isSystemRoute).collect(Collectors.toList());
+
+        mRouteItems.clear();
+        for (MediaRoute2Info route : systemRoutes) {
+            boolean isSelectedRoute = selectedRoutesIds.contains(route.getId());
+            Boolean wasTransferredBySelf = isSelectedRoute ? selectionInitiatedBySelf : null;
+            Integer routeTransferReason = isSelectedRoute ? sessionTransferReason : null;
+            mRouteItems.add(
+                    createRouteItemFor(
+                            route, isSelectedRoute, wasTransferredBySelf, routeTransferReason));
+        }
+        mOnRoutesChangedListener.run();
     }
 
     @NonNull
-    private static SystemRouteItem createRouteItemFor(@NonNull MediaRoute2Info routeInfo) {
-        return new SystemRouteItem.Builder(routeInfo.getId())
-                .setName(String.valueOf(routeInfo.getName()))
-                .setDescription(String.valueOf(routeInfo.getDescription()))
-                .build();
+    private SystemRouteItem createRouteItemFor(
+            @NonNull MediaRoute2Info routeInfo,
+            boolean isSelectedRoute,
+            @Nullable Boolean wasTransferredBySelf,
+            @Nullable Integer transferReason) {
+        SystemRouteItem.Builder builder =
+                new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
+                        .setName(String.valueOf(routeInfo.getName()))
+                        .setSelectionSupportState(
+                                isSelectedRoute
+                                        ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+                                        : SystemRouteItem.SelectionSupportState.SELECTABLE)
+                        .setDescription(String.valueOf(routeInfo.getDescription()))
+                        .setTransferInitiatedBySelf(wasTransferredBySelf)
+                        .setTransferReason(getHumanReadableTransferReason(transferReason));
+        try {
+            if (mSuitabilityStatusMethod != null) {
+                // See b/336510942 for details on why reflection is needed.
+                @SuppressLint("BanUncheckedReflection")
+                int status = (Integer) mSuitabilityStatusMethod.invoke(routeInfo);
+                builder.setSuitabilityStatus(getHumanReadableSuitabilityStatus(status));
+                // TODO: b/319645714 - Populate wasTransferInitiatedBySelf. For that we need to
+                // change the implementation of this class to use the routing controller instead
+                // of a route callback.
+            }
+        } catch (IllegalAccessException | InvocationTargetException e) {
+        }
+        return builder.build();
+    }
+
+    @NonNull
+    private String getHumanReadableSuitabilityStatus(@Nullable Integer status) {
+        if (status == null) {
+            // The route is not selected, or this Android version doesn't support suitability
+            // status.
+            return null;
+        }
+        switch (status) {
+            case 0:
+                return "SUITABLE_FOR_DEFAULT_TRANSFER";
+            case 1:
+                return "SUITABLE_FOR_MANUAL_TRANSFER";
+            case 2:
+                return "NOT_SUITABLE_FOR_TRANSFER";
+            default:
+                return "UNKNOWN(" + status + ")";
+        }
+    }
+
+    @NonNull
+    private String getHumanReadableTransferReason(@Nullable Integer transferReason) {
+        if (transferReason == null) {
+            // The route is not selected, or this Android version doesn't support transfer reason.
+            return null;
+        }
+        switch (transferReason) {
+            case 0:
+                return "FALLBACK";
+            case 1:
+                return "SYSTEM_REQUEST";
+            case 2:
+                return "APP";
+            default:
+                return "UNKNOWN(" + transferReason + ")";
+        }
+    }
+
+    @RequiresApi(34)
+    private static final class Api34Impl {
+        private Api34Impl() {}
+
+        @DoNotInline
+        static RoutingSessionInfo getRoutingSessionInfo(
+                MediaRouter2.RoutingController routingController) {
+            return routingController.getRoutingSessionInfo();
+        }
     }
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
index 438ccfc..21d112f 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.media.MediaRouter;
+import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 
@@ -34,19 +35,35 @@
     private final MediaRouter mMediaRouter;
 
     @NonNull
-    private final MediaRouter.Callback mCallback = new MediaRouter.SimpleCallback() {
-        @Override
-        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
-            super.onRouteAdded(router, info);
-            mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(info));
-        }
+    private final MediaRouter.Callback mCallback =
+            new MediaRouter.SimpleCallback() {
+                @Override
+                public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+                    mOnRoutesChangedListener.run();
+                }
 
-        @Override
-        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
-            super.onRouteRemoved(router, info);
-            mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(info));
-        }
-    };
+                @Override
+                public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteUnselected(
+                        MediaRouter router, int type, MediaRouter.RouteInfo info) {
+                    mOnRoutesChangedListener.run();
+                }
+
+                @Override
+                public void onRouteSelected(
+                        MediaRouter router, int type, MediaRouter.RouteInfo info) {
+                    mOnRoutesChangedListener.run();
+                }
+            };
 
     /** Returns a new instance. */
     @NonNull
@@ -83,23 +100,42 @@
 
         List<SystemRouteItem> out = new ArrayList<>();
 
+        MediaRouter.RouteInfo selectedRoute =
+                mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
         for (int i = 0; i < count; i++) {
             MediaRouter.RouteInfo info = mMediaRouter.getRouteAt(i);
             if (info.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL) {
                 // We are only interested in system routes.
-                out.add(createRouteItemFor(info));
+                out.add(createRouteItemFor(info, /* isSelected= */ selectedRoute == info));
             }
         }
 
         return out;
     }
 
-    @NonNull
-    private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
-        SystemRouteItem.Builder builder =
-                new SystemRouteItem.Builder(/* id= */ routeInfo.getName().toString())
-                        .setName(routeInfo.getName().toString());
+    @Override
+    public boolean select(@NonNull SystemRouteItem item) {
+        int routeCount = mMediaRouter.getRouteCount();
+        for (int i = 0; i < routeCount; i++) {
+            MediaRouter.RouteInfo route = mMediaRouter.getRouteAt(i);
+            if (TextUtils.equals(route.getName().toString(), item.mId)) {
+                mMediaRouter.selectRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO, route);
+                return true;
+            }
+        }
+        return false;
+    }
 
+    @NonNull
+    private SystemRouteItem createRouteItemFor(
+            @NonNull MediaRouter.RouteInfo routeInfo, boolean isSelected) {
+        SystemRouteItem.Builder builder =
+                new SystemRouteItem.Builder(getSourceId(), /* id= */ routeInfo.getName().toString())
+                        .setName(routeInfo.getName().toString());
+        builder.setSelectionSupportState(
+                isSelected
+                        ? SystemRouteItem.SelectionSupportState.RESELECTABLE
+                        : SystemRouteItem.SelectionSupportState.SELECTABLE);
         CharSequence description = routeInfo.getDescription();
         if (description != null) {
             builder.setDescription(String.valueOf(description));
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
index f5a32db..bad6317 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
@@ -17,7 +17,6 @@
 package com.example.androidx.mediarouting.activities.systemrouting.source;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
 import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
@@ -29,23 +28,11 @@
  */
 public abstract class SystemRoutesSource {
 
-    private static final NoOpOnRoutesChangedListener NO_OP_ON_ROUTES_CHANGED_LISTENER =
-            new NoOpOnRoutesChangedListener();
+    @NonNull protected Runnable mOnRoutesChangedListener = () -> {};
 
-    @NonNull
-    protected OnRoutesChangedListener mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
-
-    /**
-     * Sets {@link OnRoutesChangedListener} and subscribes to the source updates.
-     * To unsubscribe from the routes update pass {@code null} instead of the listener.
-     */
-    public void setOnRoutesChangedListener(
-            @Nullable OnRoutesChangedListener onRoutesChangedListener) {
-        if (onRoutesChangedListener != null) {
-            mOnRoutesChangedListener = onRoutesChangedListener;
-        } else {
-            mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
-        }
+    /** Sets a {@link Runnable} to invoke whenever routes change. */
+    public void setOnRoutesChangedListener(@NonNull Runnable onRoutesChangedListener) {
+        mOnRoutesChangedListener = onRoutesChangedListener;
     }
 
     /**
@@ -63,6 +50,12 @@
         // Empty on purpose.
     }
 
+    /** Returns a string that uniquely identifies this source. */
+    @NonNull
+    public final String getSourceId() {
+        return getClass().getSimpleName();
+    }
+
     /**
      * Gets a source item containing source type.
      */
@@ -76,39 +69,11 @@
     public abstract List<SystemRouteItem> fetchSourceRouteItems();
 
     /**
-     * An interface for listening to routes changes: whether the route has been added or removed
-     * from the source.
+     * Selects the route that corresponds to the given item.
+     *
+     * @param item An item with {@link SystemRouteItem#mSelectionSupportState} {@link
+     *     SystemRouteItem.SelectionSupportState#SELECTABLE}.
+     * @return Whether the selection was successful.
      */
-    public interface OnRoutesChangedListener {
-
-        /**
-         * Called when a route has been added to the source's routes list.
-         *
-         * @param routeItem a newly added route.
-         */
-        void onRouteAdded(@NonNull SystemRouteItem routeItem);
-
-        /**
-         * Called when a route has been removed from the source's routes list.
-         *
-         * @param routeItem a recently removed route.
-         */
-        void onRouteRemoved(@NonNull SystemRouteItem routeItem);
-    }
-
-    /**
-     * Default no-op implementation of {@link OnRoutesChangedListener}.
-     * Used as a fallback implement when there is no listener.
-     */
-    private static final class NoOpOnRoutesChangedListener implements OnRoutesChangedListener {
-        @Override
-        public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
-            // Empty on purpose.
-        }
-
-        @Override
-        public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
-            // Empty on purpose.
-        }
-    }
+    public abstract boolean select(@NonNull SystemRouteItem item);
 }
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
index 47ffed2..a597796 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
@@ -304,7 +304,16 @@
             for (String memberRouteId : mMemberRouteIds) {
                 groupDescriptorBuilder.addGroupMemberId(memberRouteId);
             }
-
+            if (!mMemberRouteIds.isEmpty()) {
+                DynamicRouteDescriptor firstDynamicRouteDescriptor =
+                        mDynamicRouteDescriptors.get(mMemberRouteIds.get(0));
+                if (firstDynamicRouteDescriptor != null) {
+                    String name = firstDynamicRouteDescriptor.getRouteDescriptor().getName();
+                    int sizeMinusOne = mMemberRouteIds.size() - 1;
+                    String nameSuffix = sizeMinusOne == 0 ? "" : (" + " + sizeMinusOne);
+                    groupDescriptorBuilder.setName(name + nameSuffix);
+                }
+            }
             mGroupDescriptor = groupDescriptorBuilder.build();
         }
 
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
index 658c256..abbfbf2 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
@@ -58,12 +58,16 @@
 
     /** Reload all routes provided by this service. */
     public void reloadRoutes() {
-        mDynamicGroupMediaRouteProvider.reloadRoutes();
+        if (mDynamicGroupMediaRouteProvider != null) {
+            mDynamicGroupMediaRouteProvider.reloadRoutes();
+        }
     }
 
     /** Reload the flag for isDynamicRouteEnabled. */
     public void reloadDynamicRoutesEnabled() {
-        mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
+        if (mDynamicGroupMediaRouteProvider != null) {
+            mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
+        }
     }
 
     /**
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
index 2572fc6..50418cb 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
@@ -91,6 +91,56 @@
             android:layout_margin="12dp"
             android:padding="4dp">
 
+            <Switch
+                android:id="@+id/enable_simple_provider_switch"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:layout_centerVertical="true" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:gravity="center"
+                android:text="Enable simple route provider" />
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_margin="12dp"
+            android:padding="4dp">
+
+            <Switch
+                android:id="@+id/enable_dynamic_provider_switch"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_alignParentEnd="true"
+                android:layout_alignParentRight="true"
+                android:layout_centerVertical="true" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:gravity="center"
+                android:text="Enable dynamic route provider" />
+
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:layout_margin="12dp"
+            android:padding="4dp">
+
             <Spinner
                 android:id="@+id/dialog_spinner"
                 android:layout_width="wrap_content"
diff --git a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
index 5d54748..8b3ac4e 100644
--- a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
+++ b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
@@ -77,6 +77,49 @@
                 android:textSize="14sp"
                 tools:text="This is a description of an amazing system route." />
 
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/route_suitability_status"
+                android:layout_marginTop="8dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/route_description"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:textSize="14sp"
+                tools:text="Suitability status." />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/route_transfer_initiated_by_self"
+                android:layout_marginTop="8dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/route_suitability_status"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:textSize="14sp"
+                tools:text="Transfer initiated by self." />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/route_transfer_reason"
+                android:layout_marginTop="8dp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/route_transfer_initiated_by_self"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:textSize="14sp"
+                tools:text="Transfer reason." />
+
+            <androidx.appcompat.widget.AppCompatButton
+                android:id="@+id/route_selection_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/route_transfer_reason"
+                android:layout_alignParentLeft="true"
+                android:layout_alignParentStart="true"
+                android:textSize="14sp"
+                tools:text="Selection button" />
+
         </RelativeLayout>
 
     </androidx.cardview.widget.CardView>
diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle
index 7f821c2..061c521 100644
--- a/samples/Support4Demos/build.gradle
+++ b/samples/Support4Demos/build.gradle
@@ -30,5 +30,6 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "com.example.android.supportv4"
 }
diff --git a/samples/SupportAnimationDemos/build.gradle b/samples/SupportAnimationDemos/build.gradle
index 5143dde..4288948 100644
--- a/samples/SupportAnimationDemos/build.gradle
+++ b/samples/SupportAnimationDemos/build.gradle
@@ -15,5 +15,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "com.example.android.support.animation"
 }
diff --git a/samples/SupportContentDemos/build.gradle b/samples/SupportContentDemos/build.gradle
index d2ab9a4..c543967 100644
--- a/samples/SupportContentDemos/build.gradle
+++ b/samples/SupportContentDemos/build.gradle
@@ -31,5 +31,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "com.example.android.support.content.demos"
 }
diff --git a/samples/SupportPreferenceDemos/build.gradle b/samples/SupportPreferenceDemos/build.gradle
index 73d482c..02d78ee 100644
--- a/samples/SupportPreferenceDemos/build.gradle
+++ b/samples/SupportPreferenceDemos/build.gradle
@@ -26,5 +26,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "com.example.androidx.preference"
-}
\ No newline at end of file
+}
diff --git a/samples/SupportRemoteCallbackDemos/build.gradle b/samples/SupportRemoteCallbackDemos/build.gradle
index 5864d41..7876ec4 100644
--- a/samples/SupportRemoteCallbackDemos/build.gradle
+++ b/samples/SupportRemoteCallbackDemos/build.gradle
@@ -27,6 +27,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
     }
     namespace "com.example.androidx.remotecallback.demos"
diff --git a/samples/SupportSliceDemos/build.gradle b/samples/SupportSliceDemos/build.gradle
index 007adcb..89d6061 100644
--- a/samples/SupportSliceDemos/build.gradle
+++ b/samples/SupportSliceDemos/build.gradle
@@ -32,6 +32,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
     }
     namespace "com.example.androidx.slice.demos"
diff --git a/samples/SupportTransitionDemos/build.gradle b/samples/SupportTransitionDemos/build.gradle
index 6119a42..eb68924 100644
--- a/samples/SupportTransitionDemos/build.gradle
+++ b/samples/SupportTransitionDemos/build.gradle
@@ -21,6 +21,7 @@
     aaptOptions {
         additionalParameters "--no-version-transitions"
     }
+    compileSdk 35
     namespace "com.example.android.support.transition"
 }
 
diff --git a/samples/SupportWearDemos/build.gradle b/samples/SupportWearDemos/build.gradle
index 252fc05..3ae3154 100644
--- a/samples/SupportWearDemos/build.gradle
+++ b/samples/SupportWearDemos/build.gradle
@@ -27,6 +27,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         minSdkVersion 24
     }
diff --git a/settings.gradle b/settings.gradle
index 8efe6fc..be19dab 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -96,7 +96,8 @@
         value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
         value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
 
-        publishing.onlyIf { it.authenticated }
+        // Do not publish scan for androidx-platform-dev
+        publishing.onlyIf { false }
     }
 }
 
@@ -1082,6 +1083,8 @@
 File repoRoot = new File(rootDir, "../..").canonicalFile
 
 includeOptionalProject(":xr:xr", new File(repoRoot, "xr/xr"), [BuildType.XR])
+includeOptionalProject(":xr:integration-tests:compose-adaptive-sample", new File(repoRoot, "xr/integration-tests/compose-adaptive-sample"), [BuildType.XR])
+includeOptionalProject(":xr:xr-material3-adaptive", new File(repoRoot, "xr/xr-material3-adaptive"), [BuildType.XR])
 
 /////////////////////////////
 //
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index 3bf9712..2e95f16 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -59,5 +59,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.slice.test"
 }
diff --git a/startup/integration-tests/first-library/build.gradle b/startup/integration-tests/first-library/build.gradle
index 3acf247..ef0402b 100644
--- a/startup/integration-tests/first-library/build.gradle
+++ b/startup/integration-tests/first-library/build.gradle
@@ -35,5 +35,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.startup.first_library"
 }
diff --git a/startup/integration-tests/second-library/build.gradle b/startup/integration-tests/second-library/build.gradle
index 68a15f7..96216cb 100644
--- a/startup/integration-tests/second-library/build.gradle
+++ b/startup/integration-tests/second-library/build.gradle
@@ -34,5 +34,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.startup.second_library"
 }
diff --git a/startup/integration-tests/test-app/build.gradle b/startup/integration-tests/test-app/build.gradle
index 93f6d4d..f7893ef 100644
--- a/startup/integration-tests/test-app/build.gradle
+++ b/startup/integration-tests/test-app/build.gradle
@@ -21,6 +21,7 @@
 }
 
 android {
+    compileSdkVersion 35
     buildTypes {
         getByName("release") {
             minifyEnabled = true
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index eed2f10..80f7aa2 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -50,5 +50,6 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "androidx.test.uiautomator.testapp"
 }
diff --git a/testutils/testutils-appcompat/build.gradle b/testutils/testutils-appcompat/build.gradle
index 5c26d7f..611244d 100644
--- a/testutils/testutils-appcompat/build.gradle
+++ b/testutils/testutils-appcompat/build.gradle
@@ -41,6 +41,7 @@
 }
 
 android {
+    compileSdk 35
     lintOptions {
         disable "InvalidPackage" // Lint is unhappy about junit package
     }
diff --git a/testutils/testutils-espresso/build.gradle b/testutils/testutils-espresso/build.gradle
index 87da559..0d449f2 100644
--- a/testutils/testutils-espresso/build.gradle
+++ b/testutils/testutils-espresso/build.gradle
@@ -38,6 +38,7 @@
 }
 
 android {
+    compileSdk 35
     lintOptions {
         disable "InvalidPackage" // Lint is unhappy about junit package
     }
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index bc3fc1d..ff23ca5 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -42,5 +42,6 @@
 }
 
 android {
+    compileSdkVersion 35
     namespace "androidx.viewpager2.integration.testapp"
 }
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 54fcef0..4eacf1f 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -21,6 +21,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         minSdk 28
     }
diff --git a/wear/compose/compose-material3/integration-tests/build.gradle b/wear/compose/compose-material3/integration-tests/build.gradle
index 19bacd4..955f6fd 100644
--- a/wear/compose/compose-material3/integration-tests/build.gradle
+++ b/wear/compose/compose-material3/integration-tests/build.gradle
@@ -55,8 +55,9 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         minSdkVersion 25
     }
     namespace "androidx.wear.compose.material3.demos"
-}
\ No newline at end of file
+}
diff --git a/wear/compose/compose-material3/samples/build.gradle b/wear/compose/compose-material3/samples/build.gradle
index 0c87364..49df01d 100644
--- a/wear/compose/compose-material3/samples/build.gradle
+++ b/wear/compose/compose-material3/samples/build.gradle
@@ -45,6 +45,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         minSdkVersion 25
     }
@@ -56,4 +57,4 @@
     type = LibraryType.SAMPLES
     inceptionYear = "2022"
     description = "Contains the sample code for the Android Wear Compose Material 3 Classes"
-}
\ No newline at end of file
+}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 0da615f..872bc9d 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index 96f05c4..117b475 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdkVersion 35
 
     namespace "androidx.wear.compose.integration.macrobenchmark.target"
     buildTypes {
@@ -53,4 +54,4 @@
     implementation(project(path:':tracing:tracing-perfetto-binary'))
 }
 
-android.defaultConfig.minSdkVersion 25
\ No newline at end of file
+android.defaultConfig.minSdkVersion 25
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index ca96d2b..b29b94ada 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -22,6 +22,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         applicationId "androidx.wear.compose.integration.navigation"
         minSdk 25
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index e72e167..23159f4a 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -52,14 +52,21 @@
 package androidx.window.extensions.embedding {
 
   public interface ActivityEmbeddingComponent {
+    method public default void clearActivityStackAttributesCalculator();
+    method public default void clearEmbeddedActivityWindowInfoCallback();
     method public void clearSplitAttributesCalculator();
     method public void clearSplitInfoCallback();
     method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
     method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+    method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+    method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+    method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
     method public default void invalidateTopVisibleSplitAttributes();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
     method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+    method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+    method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
     method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
     method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
@@ -67,12 +74,14 @@
     method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
     method public default void unpinTopActivityStack(int);
     method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+    method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
     method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
     method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
   }
 
   public class ActivityEmbeddingOptionsProperties {
     field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+    field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
   }
 
   public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -92,6 +101,7 @@
   public class ActivityStack {
     method public java.util.List<android.app.Activity!> getActivities();
     method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+    method public String? getTag();
     method public boolean isEmpty();
   }
 
@@ -102,6 +112,24 @@
     field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
   }
 
+  public final class ActivityStackAttributes {
+    method public android.graphics.Rect getRelativeBounds();
+    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+  }
+
+  public static final class ActivityStackAttributes.Builder {
+    ctor public ActivityStackAttributes.Builder();
+    method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+  }
+
+  public class ActivityStackAttributesCalculatorParams {
+    method public String getActivityStackTag();
+    method public android.os.Bundle getLaunchOptions();
+    method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+  }
+
   public abstract class AnimationBackground {
     method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
     field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
@@ -111,12 +139,68 @@
     method @ColorInt public int getColor();
   }
 
+  public final class AnimationParams {
+    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method @AnimRes public int getChangeAnimationResId();
+    method @AnimRes public int getCloseAnimationResId();
+    method @AnimRes public int getOpenAnimationResId();
+    field @AnimRes public static final int DEFAULT_ANIMATION_RESOURCES_ID = -1; // 0xffffffff
+  }
+
+  public static final class AnimationParams.Builder {
+    ctor public AnimationParams.Builder();
+    method public androidx.window.extensions.embedding.AnimationParams build();
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setChangeAnimationResId(@AnimRes int);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setCloseAnimationResId(@AnimRes int);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setOpenAnimationResId(@AnimRes int);
+  }
+
+  public final class DividerAttributes {
+    method @ColorInt public int getDividerColor();
+    method public int getDividerType();
+    method public float getPrimaryMaxRatio();
+    method public float getPrimaryMinRatio();
+    method @Dimension public int getWidthDp();
+    method public boolean isDraggingToFullscreenAllowed();
+    field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+    field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+    field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+  }
+
+  public static final class DividerAttributes.Builder {
+    ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+    ctor public DividerAttributes.Builder(int);
+    method public androidx.window.extensions.embedding.DividerAttributes build();
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+  }
+
+  public class EmbeddedActivityWindowInfo {
+    method public android.app.Activity getActivity();
+    method public android.graphics.Rect getActivityStackBounds();
+    method public android.graphics.Rect getTaskBounds();
+    method public boolean isEmbedded();
+  }
+
   public abstract class EmbeddingRule {
     method public String? getTag();
   }
 
+  public class ParentContainerInfo {
+    method public android.content.res.Configuration getConfiguration();
+    method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+    method public android.view.WindowMetrics getWindowMetrics();
+  }
+
   public class SplitAttributes {
-    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method @Deprecated public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method public androidx.window.extensions.embedding.AnimationParams getAnimationParams();
+    method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
     method public int getLayoutDirection();
     method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
     method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
@@ -124,8 +208,11 @@
 
   public static final class SplitAttributes.Builder {
     ctor public SplitAttributes.Builder();
+    ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitAttributes build();
-    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method @Deprecated public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.extensions.embedding.AnimationParams);
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
@@ -260,6 +347,24 @@
     method public android.graphics.Rect getBounds();
   }
 
+  public final class DisplayFoldFeature {
+    method public int getType();
+    method public boolean hasProperties(int...);
+    method public boolean hasProperty(int);
+    field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+    field public static final int TYPE_HINGE = 1; // 0x1
+    field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class DisplayFoldFeature.Builder {
+    ctor public DisplayFoldFeature.Builder(int);
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+    method public androidx.window.extensions.layout.DisplayFoldFeature build();
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+  }
+
   public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
     ctor public FoldingFeature(android.graphics.Rect, int, int);
     method public android.graphics.Rect getBounds();
@@ -271,9 +376,19 @@
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
+  public final class SupportedWindowFeatures {
+    method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+  }
+
+  public static final class SupportedWindowFeatures.Builder {
+    ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+    method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+  }
+
   public interface WindowLayoutComponent {
     method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+    method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
     method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
   }
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 13940ea..e005796 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -52,14 +52,21 @@
 package androidx.window.extensions.embedding {
 
   public interface ActivityEmbeddingComponent {
+    method public default void clearActivityStackAttributesCalculator();
+    method public default void clearEmbeddedActivityWindowInfoCallback();
     method public void clearSplitAttributesCalculator();
     method public void clearSplitInfoCallback();
     method @Deprecated public default void finishActivityStacks(java.util.Set<android.os.IBinder!>);
     method public default void finishActivityStacksWithTokens(java.util.Set<androidx.window.extensions.embedding.ActivityStack.Token!>);
+    method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
+    method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
+    method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
     method public default void invalidateTopVisibleSplitAttributes();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
     method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+    method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams!,androidx.window.extensions.embedding.ActivityStackAttributes!>);
+    method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.embedding.EmbeddedActivityWindowInfo!>);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
     method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
     method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function<androidx.window.extensions.embedding.SplitAttributesCalculatorParams!,androidx.window.extensions.embedding.SplitAttributes!>);
@@ -67,12 +74,14 @@
     method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
     method public default void unpinTopActivityStack(int);
     method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.ActivityStack!>!>);
+    method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
     method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
     method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
   }
 
   public class ActivityEmbeddingOptionsProperties {
     field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
+    field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
   }
 
   public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
@@ -92,6 +101,7 @@
   public class ActivityStack {
     method public java.util.List<android.app.Activity!> getActivities();
     method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
+    method public String? getTag();
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.os.IBinder getToken();
     method public boolean isEmpty();
   }
@@ -103,6 +113,24 @@
     field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
   }
 
+  public final class ActivityStackAttributes {
+    method public android.graphics.Rect getRelativeBounds();
+    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
+  }
+
+  public static final class ActivityStackAttributes.Builder {
+    ctor public ActivityStackAttributes.Builder();
+    method public androidx.window.extensions.embedding.ActivityStackAttributes build();
+    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
+    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
+  }
+
+  public class ActivityStackAttributesCalculatorParams {
+    method public String getActivityStackTag();
+    method public android.os.Bundle getLaunchOptions();
+    method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
+  }
+
   public abstract class AnimationBackground {
     method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
     field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
@@ -112,12 +140,68 @@
     method @ColorInt public int getColor();
   }
 
+  public final class AnimationParams {
+    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method @AnimRes public int getChangeAnimationResId();
+    method @AnimRes public int getCloseAnimationResId();
+    method @AnimRes public int getOpenAnimationResId();
+    field @AnimRes public static final int DEFAULT_ANIMATION_RESOURCES_ID = -1; // 0xffffffff
+  }
+
+  public static final class AnimationParams.Builder {
+    ctor public AnimationParams.Builder();
+    method public androidx.window.extensions.embedding.AnimationParams build();
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setChangeAnimationResId(@AnimRes int);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setCloseAnimationResId(@AnimRes int);
+    method public androidx.window.extensions.embedding.AnimationParams.Builder setOpenAnimationResId(@AnimRes int);
+  }
+
+  public final class DividerAttributes {
+    method @ColorInt public int getDividerColor();
+    method public int getDividerType();
+    method public float getPrimaryMaxRatio();
+    method public float getPrimaryMinRatio();
+    method @Dimension public int getWidthDp();
+    method public boolean isDraggingToFullscreenAllowed();
+    field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
+    field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
+    field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+  }
+
+  public static final class DividerAttributes.Builder {
+    ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
+    ctor public DividerAttributes.Builder(int);
+    method public androidx.window.extensions.embedding.DividerAttributes build();
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
+    method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
+  }
+
+  public class EmbeddedActivityWindowInfo {
+    method public android.app.Activity getActivity();
+    method public android.graphics.Rect getActivityStackBounds();
+    method public android.graphics.Rect getTaskBounds();
+    method public boolean isEmbedded();
+  }
+
   public abstract class EmbeddingRule {
     method public String? getTag();
   }
 
+  public class ParentContainerInfo {
+    method public android.content.res.Configuration getConfiguration();
+    method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
+    method public android.view.WindowMetrics getWindowMetrics();
+  }
+
   public class SplitAttributes {
-    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method @Deprecated public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
+    method public androidx.window.extensions.embedding.AnimationParams getAnimationParams();
+    method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
     method public int getLayoutDirection();
     method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
     method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
@@ -125,8 +209,11 @@
 
   public static final class SplitAttributes.Builder {
     ctor public SplitAttributes.Builder();
+    ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitAttributes build();
-    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method @Deprecated public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.extensions.embedding.AnimationParams);
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
     method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
@@ -261,6 +348,24 @@
     method public android.graphics.Rect getBounds();
   }
 
+  public final class DisplayFoldFeature {
+    method public int getType();
+    method public boolean hasProperties(int...);
+    method public boolean hasProperty(int);
+    field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
+    field public static final int TYPE_HINGE = 1; // 0x1
+    field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
+    field public static final int TYPE_UNKNOWN = 0; // 0x0
+  }
+
+  public static final class DisplayFoldFeature.Builder {
+    ctor public DisplayFoldFeature.Builder(int);
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
+    method public androidx.window.extensions.layout.DisplayFoldFeature build();
+    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
+  }
+
   public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
     ctor public FoldingFeature(android.graphics.Rect, int, int);
     method public android.graphics.Rect getBounds();
@@ -272,9 +377,19 @@
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
+  public final class SupportedWindowFeatures {
+    method public java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!> getDisplayFoldFeatures();
+  }
+
+  public static final class SupportedWindowFeatures.Builder {
+    ctor public SupportedWindowFeatures.Builder(java.util.List<androidx.window.extensions.layout.DisplayFoldFeature!>);
+    method public androidx.window.extensions.layout.SupportedWindowFeatures build();
+  }
+
   public interface WindowLayoutComponent {
     method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+    method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
     method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
   }
diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
new file mode 100644
index 0000000..953ace1
--- /dev/null
+++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import org.junit.Test;
+
+/**
+ * Verifies {@link ActivityStackAttributes} behavior.
+ */
+public class ActivityStackAttributesTest {
+
+    @Test
+    public void testActivityStackAttributesDefaults() {
+        final ActivityStackAttributes defaultAttrs = new ActivityStackAttributes.Builder().build();
+        assertThat(defaultAttrs.getRelativeBounds().isEmpty()).isTrue();
+        assertThat(defaultAttrs.getWindowAttributes().getDimAreaBehavior())
+                .isEqualTo(DIM_AREA_ON_ACTIVITY_STACK);
+    }
+
+    @Test
+    public void testActivityStackAttributesEqualsMatchHashCode() {
+        final ActivityStackAttributes attrs1 = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(0, 0, 10, 10))
+                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+                .build();
+
+        final ActivityStackAttributes attrs2 = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(0, 0, 10, 10))
+                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+                .build();
+
+        final ActivityStackAttributes attrs3 = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(10, 0, 20, 10))
+                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+                .build();
+
+        final ActivityStackAttributes attrs4 = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(10, 0, 20, 10))
+                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+                .build();
+
+        final ActivityStackAttributes attrs5 = new ActivityStackAttributes.Builder()
+                .setRelativeBounds(new Rect(0, 0, 10, 10))
+                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
+                .build();
+
+        assertThat(attrs1).isNotEqualTo(attrs2);
+        assertThat(attrs1.hashCode()).isNotEqualTo(attrs2.hashCode());
+        assertThat(attrs1).isNotEqualTo(attrs3);
+        assertThat(attrs1.hashCode()).isNotEqualTo(attrs3.hashCode());
+        assertThat(attrs1).isNotEqualTo(attrs4);
+        assertThat(attrs1.hashCode()).isNotEqualTo(attrs4.hashCode());
+        assertThat(attrs1).isEqualTo(attrs5);
+        assertThat(attrs1.hashCode()).isEqualTo(attrs5.hashCode());
+    }
+}
diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
new file mode 100644
index 0000000..f699b61
--- /dev/null
+++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link EmbeddedActivityWindowInfo} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmbeddedActivityWindowInfoTest {
+
+    @Mock
+    private Activity mActivity;
+    @Mock
+    private Activity mActivity2;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testGetter() {
+        final Rect taskBounds = new Rect(0, 0, 1000, 2000);
+        final Rect activityStackBounds = new Rect(0, 0, 1000, 1000);
+        final EmbeddedActivityWindowInfo info = new EmbeddedActivityWindowInfo(mActivity,
+                true /* isEmbedded */, taskBounds, activityStackBounds);
+
+        assertEquals(mActivity, info.getActivity());
+        assertTrue(info.isEmbedded());
+        assertEquals(taskBounds, info.getTaskBounds());
+        assertEquals(activityStackBounds, info.getActivityStackBounds());
+    }
+
+    @Test
+    public void testEqualsAndHashCode() {
+        final EmbeddedActivityWindowInfo info1 = new EmbeddedActivityWindowInfo(mActivity,
+                true /* isEmbedded */,
+                new Rect(0, 0, 1000, 2000),
+                new Rect(0, 0, 1000, 1000));
+        final EmbeddedActivityWindowInfo info2 = new EmbeddedActivityWindowInfo(mActivity2,
+                true /* isEmbedded */,
+                new Rect(0, 0, 1000, 2000),
+                new Rect(0, 0, 1000, 1000));
+        final EmbeddedActivityWindowInfo info3 = new EmbeddedActivityWindowInfo(mActivity,
+                false /* isEmbedded */,
+                new Rect(0, 0, 1000, 2000),
+                new Rect(0, 0, 1000, 1000));
+        final EmbeddedActivityWindowInfo info4 = new EmbeddedActivityWindowInfo(mActivity,
+                true /* isEmbedded */,
+                new Rect(0, 0, 1000, 1000),
+                new Rect(0, 0, 1000, 1000));
+        final EmbeddedActivityWindowInfo info5 = new EmbeddedActivityWindowInfo(mActivity,
+                true /* isEmbedded */,
+                new Rect(0, 0, 1000, 2000),
+                new Rect(0, 0, 1000, 1500));
+        final EmbeddedActivityWindowInfo info6 = new EmbeddedActivityWindowInfo(mActivity,
+                true /* isEmbedded */,
+                new Rect(0, 0, 1000, 2000),
+                new Rect(0, 0, 1000, 1000));
+
+        assertNotEquals(info1, info2);
+        assertNotEquals(info1, info3);
+        assertNotEquals(info1, info4);
+        assertNotEquals(info1, info5);
+        assertEquals(info1, info6);
+
+        assertNotEquals(info1.hashCode(), info2.hashCode());
+        assertNotEquals(info1.hashCode(), info3.hashCode());
+        assertNotEquals(info1.hashCode(), info4.hashCode());
+        assertNotEquals(info1.hashCode(), info5.hashCode());
+        assertEquals(info1.hashCode(), info6.hashCode());
+    }
+}
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
index 6d01778..cb53d62 100644
--- 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
@@ -158,6 +158,8 @@
     void startRearDisplaySession(@NonNull Activity activity,
             @NonNull Consumer<@WindowAreaSessionState Integer> consumer);
 
+    // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
+    // the latest library.
     /**
      * Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
      * provided in the {@code startRearDisplaySession} method. This method is only
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index d032b00..ef79b80 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -22,6 +22,8 @@
 import android.view.WindowMetrics;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.window.extensions.RequiresVendorApiLevel;
 import androidx.window.extensions.WindowExtensions;
 import androidx.window.extensions.core.util.function.Consumer;
@@ -42,6 +44,12 @@
 public interface ActivityEmbeddingComponent {
 
     /**
+     * The vendor API level of the overlay feature APIs.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    int OVERLAY_FEATURE_API_LEVEL = 8;
+
+    /**
      * Updates the rules of embedding activities that are started in the client process.
      */
     @RequiresVendorApiLevel(level = 1)
@@ -253,6 +261,93 @@
     }
 
     /**
+     * Returns the {@link ParentContainerInfo} by the {@link ActivityStack} token, or {@code null}
+     * if there's not such {@link ActivityStack} associated with the {@code token}.
+     *
+     * @param activityStackToken the token of an {@link ActivityStack}.
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    @Nullable
+    default ParentContainerInfo getParentContainerInfo(
+            @NonNull ActivityStack.Token activityStackToken) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Sets a function to compute the {@link ActivityStackAttributes} for the ActivityStack given
+     * for the current window and device state provided in
+     * {@link ActivityStackAttributesCalculatorParams} on the main thread.
+     * <p>
+     * This calculator function is only triggered if the {@link ActivityStack#getTag()} is
+     * specified. Similar to {@link #setSplitAttributesCalculator(Function)}, the calculator
+     * function could be triggered multiple times. It will be triggered whenever there's a
+     * launching standalone {@link ActivityStack} with {@link ActivityStack#getTag()} specified,
+     * or a parent window or device state update, such as device rotation, folding state change,
+     * or the host task goes to multi-window mode.
+     *
+     * @param calculator The calculator function to calculate {@link ActivityStackAttributes} based
+     *                   on {@link ActivityStackAttributesCalculatorParams}.
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    default void setActivityStackAttributesCalculator(@NonNull Function<
+            ActivityStackAttributesCalculatorParams, ActivityStackAttributes> calculator) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Clears the calculator function previously set by
+     * {@link #setActivityStackAttributesCalculator(Function)}
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    default void clearActivityStackAttributesCalculator() {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Updates {@link ActivityStackAttributes} to an {@link ActivityStack} specified with
+     * {@code token} and applies the change directly. If there's no such an {@link ActivityStack},
+     * this method is no-op.
+     *
+     * @param token The {@link ActivityStack} to update.
+     * @param activityStackAttributes The attributes to be applied
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    default void updateActivityStackAttributes(@NonNull ActivityStack.Token token,
+            @NonNull ActivityStackAttributes activityStackAttributes) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Gets the {@link ActivityStack}'s token by {@code tag}, or {@code null} if there's no
+     * {@link ActivityStack} associated with the {@code tag}. For example, the {@link ActivityStack}
+     * is dismissed before the is method is called.
+     * <p>
+     * The {@link ActivityStack} token can be obtained immediately after the {@link ActivityStack}
+     * is created. This method is usually used when Activity Embedding library wants to
+     * {@link #updateActivityStackAttributes} before receiving
+     * the {@link ActivityStack} record from the callback set by
+     * {@link  #registerActivityStackCallback}.
+     * <p>
+     * For example, an app launches an overlay container and calls
+     * {@link #updateActivityStackAttributes} immediately right before the overlay
+     * {@link ActivityStack} is received from {@link #registerActivityStackCallback}.
+     *
+     * @param tag A unique identifier of an {@link ActivityStack} if set
+     * @return The {@link ActivityStack}'s token that the tag is associated with, or {@code null}
+     * if there's no such an {@link ActivityStack}.
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    @Nullable
+    default ActivityStack.Token getActivityStackToken(@NonNull String tag) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
      * Registers a callback that notifies WindowManager Jetpack about changes in
      * {@link ActivityStack}.
      * <p>
@@ -309,4 +404,48 @@
         throw new UnsupportedOperationException("This method must not be called unless there is a"
                 + " corresponding override implementation on the device.");
     }
+
+    /**
+     * Sets a callback that notifies WindowManager Jetpack about changes for a given
+     * {@link Activity} to its {@link EmbeddedActivityWindowInfo}.
+     * <p>
+     * The callback will be invoked when the {@link EmbeddedActivityWindowInfo} is changed after
+     * the {@link Activity} is launched. Similar to {@link Activity#onConfigurationChanged}, the
+     * callback will only be invoked for visible {@link Activity}.
+     *
+     * @param executor the executor to dispatch {@link EmbeddedActivityWindowInfo} change.
+     * @param callback the callback to notify {@link EmbeddedActivityWindowInfo} change.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    default void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor,
+            @NonNull Consumer<EmbeddedActivityWindowInfo> callback) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Clears the callback previously set by
+     * {@link #setEmbeddedActivityWindowInfoCallback(Executor, Consumer)}
+     */
+    @RequiresVendorApiLevel(level = 6)
+    default void clearEmbeddedActivityWindowInfoCallback() {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
+     * Returns the {@link EmbeddedActivityWindowInfo} of the given {@link Activity}, or
+     * {@code null} if the {@link Activity} is not attached.
+     * <p>
+     * This API can be used when {@link #setEmbeddedActivityWindowInfoCallback} is not set before
+     * the Activity is attached.
+     *
+     * @param activity the {@link Activity} to get {@link EmbeddedActivityWindowInfo} for.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @Nullable
+    default EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
index 5279caa..fce31d8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
@@ -29,6 +29,17 @@
     private ActivityEmbeddingOptionsProperties() {}
 
     /**
+     * The key of the unique identifier that put into {@link android.app.ActivityOptions}.
+     * <p>
+     * Type: {@link android.os.Bundle#putString(String, String) String}
+     * <p>
+     * An {@code OverlayCreateParams} property that represents the unique identifier of the overlay
+     * container.
+     */
+    public static final String KEY_OVERLAY_TAG =
+            "androidx.window.extensions.embedding.OverlayTag";
+
+    /**
      * The key of {@link ActivityStack.Token#toBundle()} that put into
      * {@link android.app.ActivityOptions}.
      * <p>
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 69b7ad3..0441d37 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
@@ -16,12 +16,15 @@
 
 package androidx.window.extensions.embedding;
 
+import static androidx.window.extensions.embedding.ActivityEmbeddingComponent.OVERLAY_FEATURE_API_LEVEL;
+
 import android.app.Activity;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.window.extensions.RequiresVendorApiLevel;
 
@@ -43,6 +46,9 @@
     @NonNull
     private final Token mToken;
 
+    @Nullable
+    private final String mTag;
+
     /**
      * The {@code ActivityStack} constructor
      *
@@ -51,14 +57,18 @@
      * @param isEmpty Indicates whether there's any {@link Activity} running in this
      *                {@code ActivityStack}
      * @param token The token to identify this {@code ActivityStack}
+     * @param tag A unique identifier of {@link ActivityStack}. Only specifies for the overlay
+     *            standalone {@link ActivityStack} currently.
      */
-    ActivityStack(@NonNull List<Activity> activities, boolean isEmpty, @NonNull Token token) {
+    ActivityStack(@NonNull List<Activity> activities, boolean isEmpty, @NonNull Token token,
+            @Nullable String tag) {
         Objects.requireNonNull(activities);
         Objects.requireNonNull(token);
 
         mActivities = new ArrayList<>(activities);
         mIsEmpty = isEmpty;
         mToken = token;
+        mTag = tag;
     }
 
     /**
@@ -110,6 +120,15 @@
         return mToken.getRawToken();
     }
 
+    /**
+     * Returns the associated tag if specified. Otherwise, returns {@code null}.
+     */
+    @RequiresVendorApiLevel(level = OVERLAY_FEATURE_API_LEVEL)
+    @Nullable
+    public String getTag() {
+        return mTag;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -117,7 +136,8 @@
         ActivityStack that = (ActivityStack) o;
         return mActivities.equals(that.mActivities)
                 && mIsEmpty == that.mIsEmpty
-                && mToken.equals(that.mToken);
+                && mToken.equals(that.mToken)
+                && Objects.equals(mTag, that.mTag);
     }
 
     @Override
@@ -125,6 +145,7 @@
         int result = (mIsEmpty ? 1 : 0);
         result = result * 31 + mActivities.hashCode();
         result = result * 31 + mToken.hashCode();
+        result = result * 31 + Objects.hashCode(mTag);
 
         return result;
     }
@@ -135,6 +156,7 @@
         return "ActivityStack{" + "mActivities=" + mActivities
                 + ", mIsEmpty=" + mIsEmpty
                 + ", mToken=" + mToken
+                + ", mTag=" + mTag
                 + '}';
     }
 
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
new file mode 100644
index 0000000..eb4f743
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+/**
+ * Attributes used to update the layout and configuration of an {@link ActivityStack}.
+ */
+public final class ActivityStackAttributes {
+
+    @NonNull
+    private final Rect mRelativeBounds;
+
+    @NonNull
+    private final WindowAttributes mWindowAttributes;
+
+    private ActivityStackAttributes(@NonNull Rect relativeBounds,
+            @NonNull WindowAttributes windowAttributes) {
+        mRelativeBounds = relativeBounds;
+        mWindowAttributes = windowAttributes;
+    }
+
+    /**
+     * Returns the requested bounds of an {@link ActivityStack} which relative to its parent
+     * container.
+     * <p>
+     * {@link Rect#isEmpty() Empty} bounds mean that this {@link ActivityStack} should fill its
+     * parent container bounds.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Rect getRelativeBounds() {
+        return mRelativeBounds;
+    }
+
+    /**
+     * Returns the {@link WindowAttributes} which contains the configurations of the embedded
+     * Activity windows with this attributes.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public WindowAttributes getWindowAttributes() {
+        return mWindowAttributes;
+    }
+
+    @Override
+    public int hashCode() {
+        return mRelativeBounds.hashCode() * 31 + mWindowAttributes.hashCode();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof ActivityStackAttributes)) return false;
+        final ActivityStackAttributes attrs = (ActivityStackAttributes) obj;
+        return mRelativeBounds.equals(attrs.mRelativeBounds)
+                && mWindowAttributes.equals(attrs.mWindowAttributes);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return ActivityStackAttributes.class.getSimpleName() + ": {"
+                + " relativeBounds=" + mRelativeBounds
+                + ", windowAttributes=" + mWindowAttributes
+                + "}";
+    }
+
+    /** The builder class of {@link ActivityStackAttributes}. */
+    public static final class Builder {
+
+        /** The {@link ActivityStackAttributes} builder constructor. */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder() {}
+
+        @NonNull
+        private final Rect mRelativeBounds = new Rect();
+
+        @NonNull
+        private WindowAttributes mWindowAttributes =
+                new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK);
+
+        /**
+         * Sets the requested relative bounds of the {@link ActivityStack}. If this value is
+         * not specified, {@link #getRelativeBounds()} defaults to {@link Rect#isEmpty() empty}
+         * bounds, which means to follow the parent container bounds.
+         *
+         * @param relativeBounds The requested relative bounds.
+         * @return This {@code Builder}.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setRelativeBounds(@NonNull Rect relativeBounds) {
+            mRelativeBounds.set(relativeBounds);
+            return this;
+        }
+
+        /**
+         * Sets the window attributes. If this value is not specified, the
+         * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the
+         * {@link ActivityStack} of the requested activity.
+         *
+         * @param attributes The {@link WindowAttributes}
+         * @return This {@code Builder}.
+         */
+        @NonNull
+        @RequiresVendorApiLevel(level = 6)
+        public Builder setWindowAttributes(@NonNull WindowAttributes attributes) {
+            mWindowAttributes = attributes;
+            return this;
+        }
+
+        /**
+         * Builds an {@link ActivityStackAttributes} instance.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public ActivityStackAttributes build() {
+            return new ActivityStackAttributes(mRelativeBounds, mWindowAttributes);
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
new file mode 100644
index 0000000..99afdc6
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.core.util.function.Function;
+
+/**
+ * The parameter container used in standalone {@link ActivityStack} calculator function to report
+ * {@link ParentContainerInfo} and associated {@link ActivityStack#getTag()} to calculate
+ * {@link ActivityStackAttributes} when there's a parent container information update or a
+ * standalone {@link ActivityStack} is going to be launched.
+ *
+ * @see ActivityEmbeddingComponent#setActivityStackAttributesCalculator(Function)
+ */
+public class ActivityStackAttributesCalculatorParams {
+
+    @NonNull
+    private final ParentContainerInfo mParentContainerInfo;
+
+    @NonNull
+    private final String mActivityStackTag;
+
+    @NonNull
+    private final Bundle mLaunchOptions;
+
+    /**
+     * {@code ActivityStackAttributesCalculatorParams} constructor.
+     *
+     * @param parentContainerInfo The {@link ParentContainerInfo} of the standalone
+     *                            {@link ActivityStack} to apply the
+     *                            {@link ActivityStackAttributes}.
+     * @param activityStackTag The unique identifier of {@link ActivityStack} to apply the
+     *                         {@link ActivityStackAttributes}.
+     * @param launchOptions The options to launch the {@link ActivityStack}.
+     */
+    ActivityStackAttributesCalculatorParams(@NonNull ParentContainerInfo parentContainerInfo,
+            @NonNull String activityStackTag, @NonNull Bundle launchOptions) {
+        mParentContainerInfo = parentContainerInfo;
+        mActivityStackTag = activityStackTag;
+        mLaunchOptions = launchOptions;
+    }
+
+    /**
+     * Returns {@link ParentContainerInfo} of the standalone {@link ActivityStack} to calculate.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public ParentContainerInfo getParentContainerInfo() {
+        return mParentContainerInfo;
+    }
+
+    /**
+     * Returns unique identifier of the standalone {@link ActivityStack} to calculate.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public String getActivityStackTag() {
+        return mActivityStackTag;
+    }
+
+    /**
+     * Returns options that passed from WM Jetpack to WM Extensions library to launch an
+     * {@link ActivityStack}. {@link Bundle#isEmpty() empty} options mean there's no launch options.
+     * <p>
+     * For example, an {@link ActivityStack} launch options could be an
+     * {@link android.app.ActivityOptions} bundle that contains information to build an overlay
+     * {@link ActivityStack}.
+     * <p>
+     * The launch options will be used for initializing standalone {@link ActivityStack} with
+     * {@link #getActivityStackTag()} specified. The logic is owned by WM Jetpack, which is usually
+     * from the {@link android.app.ActivityOptions}, WM Extensions library must not touch the
+     * options.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Bundle getLaunchOptions() {
+        return mLaunchOptions;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return ActivityStackAttributesCalculatorParams.class.getSimpleName() + ":{"
+                + "parentContainerInfo=" + mParentContainerInfo
+                + "activityStackTag=" + mActivityStackTag
+                + "launchOptions=" + mLaunchOptions
+                + "}";
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java
new file mode 100644
index 0000000..53aedbb
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationParams.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import android.content.res.Resources;
+
+import androidx.annotation.AnimRes;
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.Objects;
+
+/**
+ * A class to represent the animation parameters to use while animating embedding activity
+ * containers.
+ *
+ * @see SplitAttributes.Builder#setAnimationParams
+ */
+public final class AnimationParams {
+
+    /**
+     * The default value for animation resources ID, which means to use the system default
+     * animation.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    @SuppressWarnings("ResourceType") // Use as a hint to use the system default animation.
+    @AnimRes
+    public static final int DEFAULT_ANIMATION_RESOURCES_ID = 0xFFFFFFFF;
+
+    @NonNull
+    private final AnimationBackground mAnimationBackground;
+
+    @AnimRes
+    private final int mOpenAnimationResId;
+
+    @AnimRes
+    private final int mCloseAnimationResId;
+
+    @AnimRes
+    private final int mChangeAnimationResId;
+
+    /**
+     * Creates an instance of this {@code AnimationParams}.
+     *
+     * @param animationBackground   The {@link AnimationBackground} to use for the animation
+     *                              involving this {@code AnimationParams} object.
+     * @param openAnimationResId    The animation resources ID from the "android" package to use for
+     *                              open transitions.
+     * @param closeAnimationResId   The animation resources ID from the "android" package to use for
+     *                              close transitions.
+     * @param changeAnimationResId  The animation resources ID from the "android" package to use for
+     *                              change (resize or move) transitions.
+     */
+    private AnimationParams(@NonNull AnimationBackground animationBackground,
+            @AnimRes int openAnimationResId, @AnimRes int closeAnimationResId,
+            @AnimRes int changeAnimationResId) {
+        mAnimationBackground = animationBackground;
+        mOpenAnimationResId = openAnimationResId;
+        mCloseAnimationResId = closeAnimationResId;
+        mChangeAnimationResId = changeAnimationResId;
+    }
+
+    /**
+     * Returns the {@link AnimationBackground} to use for the background during the animation.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    @NonNull
+    public AnimationBackground getAnimationBackground() {
+        return mAnimationBackground;
+    }
+
+    /**
+     * Gets the open animation.
+     *
+     * @return the open animation transition resources ID from the "android" package.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    @AnimRes
+    public int getOpenAnimationResId() {
+        return mOpenAnimationResId;
+    }
+
+    /**
+     * Gets the close animation.
+     *
+     * @return the close animation transition resources ID from the "android" package.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    @AnimRes
+    public int getCloseAnimationResId() {
+        return mCloseAnimationResId;
+    }
+
+    /**
+     * Gets the change (resize or move) animation.
+     *
+     * @return the change (resize or move) animation transition resources ID from the "android"
+     * package.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    @AnimRes
+    public int getChangeAnimationResId() {
+        return mChangeAnimationResId;
+    }
+
+    /**
+     * Builder for creating an instance of {@link AnimationParams}.
+     *
+     * - The default animation background is to use the current theme window background color.
+     * - The default animation resources ID's for transitions is the system default.
+     */
+    public static final class Builder {
+        @NonNull
+        private AnimationBackground mAnimationBackground =
+                AnimationBackground.ANIMATION_BACKGROUND_DEFAULT;
+
+        @AnimRes
+        private int mOpenAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+        @AnimRes
+        private int mCloseAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+        @AnimRes
+        private int mChangeAnimationResId = DEFAULT_ANIMATION_RESOURCES_ID;
+
+        /** Creates a new {@link AnimationParams.Builder} to create {@link AnimationParams}. */
+        @RequiresVendorApiLevel(level = 7)
+        public Builder() {
+        }
+
+        /**
+         * Sets the {@link AnimationBackground} to use for the background during the animation.
+         * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which
+         * means to use the current theme window background color.
+         *
+         * @param background An {@link AnimationBackground} to be used for the animation.
+         * @return this {@code Builder}.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public AnimationParams.Builder setAnimationBackground(
+                @NonNull AnimationBackground background) {
+            mAnimationBackground = background;
+            return this;
+        }
+
+        /**
+         * Sets the open animation.
+         * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+         * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+         *
+         * @param resId The resources ID to set from the "android" package.
+         * @return this {@code Builder}.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public AnimationParams.Builder setOpenAnimationResId(@AnimRes int resId) {
+            mOpenAnimationResId = resId;
+            return this;
+        }
+
+        /**
+         * Sets the close animation.
+         * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+         * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+         *
+         * @param resId The resources ID to set from the "android" package.
+         * @return this {@code Builder}.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public AnimationParams.Builder setCloseAnimationResId(@AnimRes int resId) {
+            mCloseAnimationResId = resId;
+            return this;
+        }
+
+        /**
+         * Sets the change (resize or move) animation.
+         * Use {@link #DEFAULT_ANIMATION_RESOURCES_ID} for the system default animation.
+         * Use {@code 0} or {@link Resources#ID_NULL} for no animation.
+         *
+         * @param resId The resources ID to set from the "android" package.
+         * @return this {@code Builder}.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public AnimationParams.Builder setChangeAnimationResId(@AnimRes int resId) {
+            mChangeAnimationResId = resId;
+            return this;
+        }
+
+        /**
+         * Builds a {@link AnimationParams} instance with the attributes
+         * specified by {@link #setAnimationBackground(AnimationBackground)},
+         * {@link #setOpenAnimationResId(int)}, {@link #setCloseAnimationResId(int)},
+         * and {@link #setChangeAnimationResId(int)}.
+         *
+         * @return the new {@code AnimationParams} instance.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public AnimationParams build() {
+            return new AnimationParams(mAnimationBackground, mOpenAnimationResId,
+                    mCloseAnimationResId, mChangeAnimationResId);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof AnimationParams)) return false;
+        AnimationParams that = (AnimationParams) o;
+        return mAnimationBackground.equals(that.mAnimationBackground)
+                && mOpenAnimationResId == that.mOpenAnimationResId
+                && mCloseAnimationResId == that.mCloseAnimationResId
+                && mChangeAnimationResId == that.mChangeAnimationResId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAnimationBackground, mOpenAnimationResId, mCloseAnimationResId,
+                mChangeAnimationResId);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return AnimationParams.class.getSimpleName() + "{"
+                + "animationBackground=" + mAnimationBackground
+                + ", openAnimation=" + mOpenAnimationResId
+                + ", closeAnimation=" + mCloseAnimationResId
+                + ", changeAnimation=" + mChangeAnimationResId
+                + "}";
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
new file mode 100644
index 0000000..f38daff
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The attributes of the divider layout and behavior.
+ *
+ * @see SplitAttributes.Builder#setDividerAttributes(DividerAttributes)
+ */
+public final class DividerAttributes {
+
+    /**
+     * A divider type that draws a static line between the primary and secondary containers.
+     */
+    public static final int DIVIDER_TYPE_FIXED = 1;
+
+    /**
+     * A divider type that draws a line between the primary and secondary containers with a drag
+     * handle that the user can drag and resize the containers.
+     */
+    public static final int DIVIDER_TYPE_DRAGGABLE = 2;
+
+    @IntDef({DIVIDER_TYPE_FIXED, DIVIDER_TYPE_DRAGGABLE})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface DividerType {
+    }
+
+    /**
+     * A special value to indicate that the ratio is unset. which means the system will choose a
+     * default value based on the display size and form factor.
+     *
+     * @see #getPrimaryMinRatio()
+     * @see #getPrimaryMaxRatio()
+     */
+    public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
+
+    /**
+     * A special value to indicate that the width is unset. which means the system will choose a
+     * default value based on the display size and form factor.
+     *
+     * @see #getWidthDp()
+     */
+    public static final int WIDTH_SYSTEM_DEFAULT = -1;
+
+    /** The {@link DividerType}. */
+    private final @DividerType int mDividerType;
+
+    /**
+     * The divider width in dp. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the system
+     * will choose a default value based on the display size and form factor.
+     */
+    private final @Dimension int mWidthDp;
+
+    /**
+     * The min split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
+     * the system will choose a default value based on the display size and form factor. Will only
+     * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+     * beyond this ratio, and when dragging is finished, the system will choose to either fully
+     * expand the secondary container or move the divider back to this ratio.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+     * drag beyond this ratio.
+     *
+     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+     */
+    private final float mPrimaryMinRatio;
+
+    /**
+     * The max split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
+     * the system will choose a default value based on the display size and form factor. Will only
+     * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+     * beyond this ratio, and when dragging is finished, the system will choose to either fully
+     * expand the primary container or move the divider back to this ratio.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+     * drag beyond this ratio.
+     *
+     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+     */
+    private final float mPrimaryMaxRatio;
+
+    /** The color of the divider. */
+    private final @ColorInt int mDividerColor;
+
+    /** Whether it is allowed to expand a container to full screen by dragging the divider. */
+    private final boolean mIsDraggingToFullscreenAllowed;
+
+    /**
+     * Constructor of {@link DividerAttributes}.
+     *
+     * @param dividerType                   the divider type. See {@link DividerType}.
+     * @param widthDp                       the width of the divider.
+     * @param primaryMinRatio               the min split ratio for the primary container.
+     * @param primaryMaxRatio               the max split ratio for the primary container.
+     * @param dividerColor                  the color of the divider.
+     * @param isDraggingToFullscreenAllowed whether it is allowed to expand a container to full
+     *                                      screen by dragging the divider.
+     * @throws IllegalStateException if the provided values are invalid.
+     */
+    private DividerAttributes(
+            @DividerType int dividerType,
+            @Dimension int widthDp,
+            float primaryMinRatio,
+            float primaryMaxRatio,
+            @ColorInt int dividerColor,
+            boolean isDraggingToFullscreenAllowed) {
+        if (dividerType == DIVIDER_TYPE_FIXED
+                && (primaryMinRatio != RATIO_SYSTEM_DEFAULT
+                || primaryMaxRatio != RATIO_SYSTEM_DEFAULT)) {
+            throw new IllegalStateException(
+                    "primaryMinRatio and primaryMaxRatio must be RATIO_SYSTEM_DEFAULT for "
+                            + "DIVIDER_TYPE_FIXED.");
+        }
+        if (primaryMinRatio != RATIO_SYSTEM_DEFAULT && primaryMaxRatio != RATIO_SYSTEM_DEFAULT
+                && primaryMinRatio > primaryMaxRatio) {
+            throw new IllegalStateException(
+                    "primaryMinRatio must be less than or equal to primaryMaxRatio");
+        }
+        mDividerType = dividerType;
+        mWidthDp = widthDp;
+        mPrimaryMinRatio = primaryMinRatio;
+        mPrimaryMaxRatio = primaryMaxRatio;
+        mDividerColor = dividerColor;
+        mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
+    }
+
+    /**
+     * Returns the divider type.
+     *
+     * @see #DIVIDER_TYPE_FIXED
+     * @see #DIVIDER_TYPE_DRAGGABLE
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public @DividerType int getDividerType() {
+        return mDividerType;
+    }
+
+    /**
+     * Returns the width of the divider. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means
+     * the system will choose a default value based on the display size and form factor.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public @Dimension int getWidthDp() {
+        return mWidthDp;
+    }
+
+    /**
+     * Returns the min split ratio for the primary container the divider can be dragged to. It
+     * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+     * based on the display size and form factor. Will only be used when the divider type is
+     * {@link #DIVIDER_TYPE_DRAGGABLE}.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+     * beyond this ratio, and when dragging is finished, the system will choose to either fully
+     * expand the secondary container or move the divider back to this ratio.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+     * drag beyond this ratio.
+     *
+     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public float getPrimaryMinRatio() {
+        return mPrimaryMinRatio;
+    }
+
+    /**
+     * Returns the max split ratio for the primary container the divider can be dragged to. It
+     * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+     * based on the display size and form factor. Will only be used when the divider type is
+     * {@link #DIVIDER_TYPE_DRAGGABLE}.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+     * beyond this ratio, and when dragging is finished, the system will choose to either fully
+     * expand the primary container or move the divider back to this ratio.
+     *
+     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+     * drag beyond this ratio.
+     *
+     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public float getPrimaryMaxRatio() {
+        return mPrimaryMaxRatio;
+    }
+
+    /** Returns the color of the divider. */
+    @RequiresVendorApiLevel(level = 6)
+    public @ColorInt int getDividerColor() {
+        return mDividerColor;
+    }
+
+    /**
+     * Returns whether it is allowed to expand a container to full screen by dragging the
+     * divider. Default is {@code true}.
+     */
+    @RequiresVendorApiLevel(level = 7)
+    public boolean isDraggingToFullscreenAllowed() {
+        return mIsDraggingToFullscreenAllowed;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof DividerAttributes)) return false;
+        final DividerAttributes other = (DividerAttributes) obj;
+        return mDividerType == other.mDividerType
+                && mWidthDp == other.mWidthDp
+                && mPrimaryMinRatio == other.mPrimaryMinRatio
+                && mPrimaryMaxRatio == other.mPrimaryMaxRatio
+                && mDividerColor == other.mDividerColor
+                && mIsDraggingToFullscreenAllowed == other.mIsDraggingToFullscreenAllowed;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDividerType, mWidthDp, mPrimaryMinRatio, mPrimaryMaxRatio,
+                mIsDraggingToFullscreenAllowed);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return DividerAttributes.class.getSimpleName() + "{"
+                + "dividerType=" + mDividerType
+                + ", width=" + mWidthDp
+                + ", minPrimaryRatio=" + mPrimaryMinRatio
+                + ", maxPrimaryRatio=" + mPrimaryMaxRatio
+                + ", dividerColor=" + mDividerColor
+                + ", isDraggingToFullscreenAllowed=" + mIsDraggingToFullscreenAllowed
+                + "}";
+    }
+
+    /** The {@link DividerAttributes} builder. */
+    public static final class Builder {
+
+        private final @DividerType int mDividerType;
+
+        private @Dimension int mWidthDp = WIDTH_SYSTEM_DEFAULT;
+
+        private float mPrimaryMinRatio = RATIO_SYSTEM_DEFAULT;
+
+        private float mPrimaryMaxRatio = RATIO_SYSTEM_DEFAULT;
+
+        private @ColorInt int mDividerColor = Color.BLACK;
+
+        private boolean mIsDraggingToFullscreenAllowed = false;
+
+        /**
+         * The {@link DividerAttributes} builder constructor.
+         *
+         * @param dividerType the divider type, possible values are {@link #DIVIDER_TYPE_FIXED} and
+         *                    {@link #DIVIDER_TYPE_DRAGGABLE}.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder(@DividerType int dividerType) {
+            mDividerType = dividerType;
+        }
+
+        /**
+         * The {@link DividerAttributes} builder constructor initialized by an existing
+         * {@link DividerAttributes}.
+         *
+         * @param original the original {@link DividerAttributes} to initialize the {@link Builder}.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder(@NonNull DividerAttributes original) {
+            Objects.requireNonNull(original);
+            mDividerType = original.mDividerType;
+            mWidthDp = original.mWidthDp;
+            mPrimaryMinRatio = original.mPrimaryMinRatio;
+            mPrimaryMaxRatio = original.mPrimaryMaxRatio;
+            mDividerColor = original.mDividerColor;
+            mIsDraggingToFullscreenAllowed = original.mIsDraggingToFullscreenAllowed;
+        }
+
+        /**
+         * Sets the divider width. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the
+         * system will choose a default value based on the display size and form factor.
+         *
+         * @throws IllegalArgumentException if the provided value is invalid.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setWidthDp(@Dimension int widthDp) {
+            if (widthDp != WIDTH_SYSTEM_DEFAULT && widthDp < 0) {
+                throw new IllegalArgumentException(
+                        "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT.");
+            }
+            mWidthDp = widthDp;
+            return this;
+        }
+
+        /**
+         * Sets the min split ratio for the primary container. It defaults to
+         * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value based
+         * on the display size and form factor. Will only be used when the divider type is
+         * {@link #DIVIDER_TYPE_DRAGGABLE}.
+         *
+         * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+         * beyond this ratio, and when dragging is finished, the system will choose to either fully
+         * expand the secondary container or move the divider back to this ratio.
+         *
+         * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+         * drag beyond this ratio.
+         *
+         * @param primaryMinRatio the min ratio for the primary container. Must be in range
+         *                        [0.0, 1.0) or {@link #RATIO_SYSTEM_DEFAULT}.
+         * @throws IllegalArgumentException if the provided value is invalid.
+         * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setPrimaryMinRatio(float primaryMinRatio) {
+            if (primaryMinRatio != RATIO_SYSTEM_DEFAULT
+                    && (primaryMinRatio >= 1.0 || primaryMinRatio < 0.0)) {
+                throw new IllegalArgumentException(
+                        "primaryMinRatio must be in [0.0, 1.0) or RATIO_SYSTEM_DEFAULT.");
+            }
+            mPrimaryMinRatio = primaryMinRatio;
+            return this;
+        }
+
+        /**
+         * Sets the max split ratio for the primary container. It defaults to
+         * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
+         * based on the display size and form factor. Will only be used when the divider type is
+         * {@link #DIVIDER_TYPE_DRAGGABLE}.
+         *
+         * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
+         * beyond this ratio, and when dragging is finished, the system will choose to either fully
+         * expand the primary container or move the divider back to this ratio.
+         *
+         * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
+         * drag beyond this ratio.
+         *
+         * @param primaryMaxRatio the max ratio for the primary container. Must be in range
+         *                        (0.0, 1.0] or {@link #RATIO_SYSTEM_DEFAULT}.
+         * @throws IllegalArgumentException if the provided value is invalid.
+         * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setPrimaryMaxRatio(float primaryMaxRatio) {
+            if (primaryMaxRatio != RATIO_SYSTEM_DEFAULT
+                    && (primaryMaxRatio > 1.0 || primaryMaxRatio <= 0.0)) {
+                throw new IllegalArgumentException(
+                        "primaryMaxRatio must be in (0.0, 1.0] or RATIO_SYSTEM_DEFAULT.");
+            }
+            mPrimaryMaxRatio = primaryMaxRatio;
+            return this;
+        }
+
+        /**
+         * Sets the color of the divider. If not set, the default color {@link Color#BLACK} is
+         * used.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setDividerColor(@ColorInt int dividerColor) {
+            mDividerColor = dividerColor;
+            return this;
+        }
+
+        /**
+         * Sets whether it is allowed to expand a container to full screen by dragging the divider.
+         * Default is {@code true}.
+         */
+        @RequiresVendorApiLevel(level = 7)
+        @NonNull
+        public Builder setDraggingToFullscreenAllowed(boolean isDraggingToFullscreenAllowed) {
+            mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
+            return this;
+        }
+
+        /**
+         * Builds a {@link DividerAttributes} instance.
+         *
+         * @return a {@link DividerAttributes} instance.
+         * @throws IllegalArgumentException if the provided values are invalid.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public DividerAttributes build() {
+            return new DividerAttributes(mDividerType, mWidthDp, mPrimaryMinRatio,
+                    mPrimaryMaxRatio, mDividerColor, mIsDraggingToFullscreenAllowed);
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
new file mode 100644
index 0000000..afea328
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.Objects;
+
+/**
+ * Describes the embedded window related info of an activity.
+ *
+ * @see ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback
+ * @see ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo
+ */
+public class EmbeddedActivityWindowInfo {
+
+    @NonNull
+    private final Activity mActivity;
+    private final boolean mIsEmbedded;
+    @NonNull
+    private final Rect mTaskBounds;
+    @NonNull
+    private final Rect mActivityStackBounds;
+
+    EmbeddedActivityWindowInfo(@NonNull Activity activity, boolean isEmbedded,
+            @NonNull Rect taskBounds, @NonNull Rect activityStackBounds) {
+        mActivity = Objects.requireNonNull(activity);
+        mIsEmbedded = isEmbedded;
+        mTaskBounds = Objects.requireNonNull(taskBounds);
+        mActivityStackBounds = Objects.requireNonNull(activityStackBounds);
+    }
+
+    /**
+     * Returns the {@link Activity} this {@link EmbeddedActivityWindowInfo} is about.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    /**
+     * Whether this activity is embedded, which means it is in an ActivityStack window that
+     * doesn't fill the Task.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public boolean isEmbedded() {
+        return mIsEmbedded;
+    }
+
+    /**
+     * Returns the bounds of the Task window in display space.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Rect getTaskBounds() {
+        return mTaskBounds;
+    }
+
+    /**
+     * Returns the bounds of the ActivityStack window in display space.
+     * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when
+     * the activity is not embedded.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Rect getActivityStackBounds() {
+        return mActivityStackBounds;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof EmbeddedActivityWindowInfo)) return false;
+        final EmbeddedActivityWindowInfo that = (EmbeddedActivityWindowInfo) o;
+        return mActivity.equals(that.mActivity)
+                && mIsEmbedded == that.mIsEmbedded
+                && mTaskBounds.equals(that.mTaskBounds)
+                && mActivityStackBounds.equals(that.mActivityStackBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mActivity.hashCode();
+        result = result * 31 + (mIsEmbedded ? 1 : 0);
+        result = result * 31 + mTaskBounds.hashCode();
+        result = result * 31 + mActivityStackBounds.hashCode();
+        return result;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "EmbeddedActivityWindowInfo{"
+                + "activity=" + mActivity
+                + ", isEmbedded=" + mIsEmbedded
+                + ", taskBounds=" + mTaskBounds
+                + ", activityStackBounds=" + mActivityStackBounds
+                + "}";
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
new file mode 100644
index 0000000..e08b803
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import android.content.res.Configuration;
+import android.view.WindowMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.layout.WindowLayoutInfo;
+
+/**
+ * The parent container information of an {@link ActivityStack}.
+ * The data class is designed to provide information to calculate the presentation of
+ * an {@link ActivityStack}.
+ */
+@RequiresVendorApiLevel(level = 6)
+public class ParentContainerInfo {
+    @NonNull
+    private final WindowMetrics mWindowMetrics;
+
+    @NonNull
+    private final Configuration mConfiguration;
+
+    @NonNull
+    private final WindowLayoutInfo mWindowLayoutInfo;
+
+    /**
+     * {@link ParentContainerInfo} constructor, which is used in Window Manager Extensions to
+     * provide information of a parent window container.
+     *
+     * @param windowMetrics The parent container's {@link WindowMetrics}
+     * @param configuration The parent container's {@link Configuration}
+     * @param windowLayoutInfo The parent container's {@link WindowLayoutInfo}
+     */
+    ParentContainerInfo(@NonNull WindowMetrics windowMetrics, @NonNull Configuration configuration,
+            @NonNull WindowLayoutInfo windowLayoutInfo) {
+        mWindowMetrics = windowMetrics;
+        mConfiguration = configuration;
+        mWindowLayoutInfo = windowLayoutInfo;
+    }
+
+    /** Returns the parent container's {@link WindowMetrics}. */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public WindowMetrics getWindowMetrics() {
+        return mWindowMetrics;
+    }
+
+    /** Returns the parent container's {@link Configuration}. */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public Configuration getConfiguration() {
+        return mConfiguration;
+    }
+
+    /** Returns the parent container's {@link WindowLayoutInfo}. */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    public WindowLayoutInfo getWindowLayoutInfo() {
+        return mWindowLayoutInfo;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mWindowMetrics.hashCode();
+        result = 31 * result + mConfiguration.hashCode();
+        result = 31 * result + mWindowLayoutInfo.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof ParentContainerInfo)) return false;
+        final ParentContainerInfo parentContainerInfo = (ParentContainerInfo) obj;
+        return mWindowMetrics.equals(parentContainerInfo.mWindowMetrics)
+                && mConfiguration.equals(parentContainerInfo.mConfiguration)
+                && mWindowLayoutInfo.equals(parentContainerInfo.mWindowLayoutInfo);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return ParentContainerInfo.class.getSimpleName() + ": {"
+                + "windowMetrics=" + WindowMetricsCompat.toString(mWindowMetrics)
+                + ", configuration=" + mConfiguration
+                + ", windowLayoutInfo=" + mWindowLayoutInfo
+                + "}";
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
index 844443e..6d43da8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -65,7 +65,7 @@
  *
  * @see SplitAttributes.SplitType
  * @see SplitAttributes.LayoutDirection
- * @see AnimationBackground
+ * @see AnimationParams
  */
 @RequiresVendorApiLevel(level = 2)
 public class SplitAttributes {
@@ -120,7 +120,7 @@
         /**
          * A window split that's based on the ratio of the size of the primary container to the
          * size of the parent window (excluding area unavailable for the containers such as the
-         * divider.
+         * divider. See {@link DividerAttributes}).
          *
          * <p>Values in the non-inclusive range (0.0, 1.0) define the size of
          * the primary container relative to the size of the parent window:
@@ -144,7 +144,7 @@
              *
              * @param ratio The proportion of the parent window occupied by the primary container
              *              of the split (excluding area unavailable for the containers such as
-             *              the divider. Can be a value in the
+             *              the divider. See {@link DividerAttributes}). Can be a value in the
              *              non-inclusive range (0.0, 1.0). Use
              *              {@link SplitType.ExpandContainersSplitType} to create a split
              *              type that occupies the entire parent window.
@@ -163,7 +163,7 @@
             /**
              * Gets the proportion of the parent window occupied by the primary activity
              * container of the split (excluding area unavailable for the containers such as the
-             * divider.
+             * divider. See {@link DividerAttributes}) .
              *
              * @return The proportion of the split occupied by the primary
              * container.
@@ -378,11 +378,15 @@
     private final SplitType mSplitType;
 
     @NonNull
-    private final AnimationBackground mAnimationBackground;
+    private final AnimationParams mAnimationParams;
 
     @NonNull
     private final WindowAttributes mWindowAttributes;
 
+    /** The attributes of a divider. If {@code null}, no divider is requested. */
+    @Nullable
+    private final DividerAttributes mDividerAttributes;
+
     /**
      * Creates an instance of this {@code SplitAttributes}.
      *
@@ -391,22 +395,25 @@
      * @param layoutDirection     The layout direction of the split, such as left to
      *                            right or top to bottom. See
      *                            {@link SplitAttributes.LayoutDirection}.
-     * @param animationBackground The {@link AnimationBackground} to use for the during animation
-     *                            of the split involving this {@code SplitAttributes} object if the
-     *                            animation requires a background.
+     * @param animationParams     The {@link AnimationParams} to use for the during animation
+     *                            of the split involving this {@code SplitAttributes} object.
      * @param attributes          The {@link WindowAttributes} of the split, such as dim area
      *                            behavior.
+     * @param dividerAttributes   The {@link DividerAttributes}. If {@code null}, no divider is
+     *                            requested.
      */
     SplitAttributes(
             @NonNull SplitType splitType,
             @ExtLayoutDirection int layoutDirection,
-            @NonNull AnimationBackground animationBackground,
-            @NonNull WindowAttributes attributes
+            @NonNull AnimationParams animationParams,
+            @NonNull WindowAttributes attributes,
+            @Nullable DividerAttributes dividerAttributes
     ) {
         mSplitType = splitType;
         mLayoutDirection = layoutDirection;
-        mAnimationBackground = animationBackground;
+        mAnimationParams = animationParams;
         mWindowAttributes = attributes;
+        mDividerAttributes = dividerAttributes;
     }
 
     /**
@@ -430,13 +437,25 @@
     }
 
     /**
-     * Returns the {@link AnimationBackground} to use for the background during the
-     * animation of the split involving this {@code SplitAttributes} object.
+     * @deprecated Use {@link #getAnimationParams()} starting with vendor API level 7. Only used if
+     * {@link #getAnimationParams()} can't be called on vendor API level 5 and 6.
      */
     @NonNull
-    @RequiresVendorApiLevel(level = 5)
+    @RequiresVendorApiLevel(level = 5, deprecatedSince = 7)
+    @Deprecated
+    @SuppressWarnings("Deprecation")
     public AnimationBackground getAnimationBackground() {
-        return mAnimationBackground;
+        return mAnimationParams.getAnimationBackground();
+    }
+
+    /**
+     * Returns the {@link AnimationParams} to use during the animation of the split involving
+     * this {@code SplitAttributes} object.
+     */
+    @NonNull
+    @RequiresVendorApiLevel(level = 7)
+    public AnimationParams getAnimationParams() {
+        return mAnimationParams;
     }
 
     /**
@@ -449,6 +468,13 @@
         return mWindowAttributes;
     }
 
+    /** Returns the {@link DividerAttributes}. If {@code null}, no divider is requested. */
+    @RequiresVendorApiLevel(level = 6)
+    @Nullable
+    public DividerAttributes getDividerAttributes() {
+        return mDividerAttributes;
+    }
+
     /**
      * Builder for creating an instance of {@link SplitAttributes}.
      *
@@ -463,18 +489,34 @@
         private int mLayoutDirection = LOCALE;
 
         @NonNull
-        private AnimationBackground mAnimationBackground =
-                AnimationBackground.ANIMATION_BACKGROUND_DEFAULT;
+        private AnimationParams mAnimationParams = new AnimationParams.Builder().build();
 
         @NonNull
         private WindowAttributes mWindowAttributes =
                 new WindowAttributes(DIM_AREA_ON_TASK);
 
+        @Nullable
+        private DividerAttributes mDividerAttributes;
+
         /** Creates a new {@link Builder} to create {@link SplitAttributes}. */
         public Builder() {
         }
 
         /**
+         * Creates a {@link Builder} with values cloned from the original {@link SplitAttributes}.
+         *
+         * @param original the original {@link SplitAttributes} to initialize the {@link Builder}.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder(@NonNull SplitAttributes original) {
+            mSplitType = original.mSplitType;
+            mLayoutDirection = original.mLayoutDirection;
+            mAnimationParams = original.mAnimationParams;
+            mWindowAttributes = original.mWindowAttributes;
+            mDividerAttributes = original.mDividerAttributes;
+        }
+
+        /**
          * Sets the split type attribute.
          *
          * The default is an equal split between primary and secondary
@@ -514,21 +556,31 @@
         }
 
         /**
-         * Sets the {@link AnimationBackground} to use for the background during the
-         * animation of the split involving this {@code SplitAttributes} object
-         * if the animation requires a background.
+         * @deprecated Use {@link #setAnimationParams(AnimationParams)} starting with vendor API
+         * level 7. Only used if {@link #setAnimationParams(AnimationParams)} can't be called on
+         * vendor API level 5 and 6.
+         */
+        @NonNull
+        @RequiresVendorApiLevel(level = 5, deprecatedSince = 7)
+        @Deprecated
+        @SuppressWarnings("Deprecation")
+        public Builder setAnimationBackground(@NonNull AnimationBackground background) {
+            mAnimationParams =
+                    new AnimationParams.Builder().setAnimationBackground(background).build();
+            return this;
+        }
+
+        /**
+         * Sets the {@link AnimationParams} to use during the animation of the split involving this
+         * {@code SplitAttributes} object.
          *
-         * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which
-         * means to use the current theme window background color.
-         *
-         * @param background An {@link AnimationBackground} to be used for the animation of the
-         *                   split.
+         * @param params The {@link AnimationParams} to be used for the animation of the split.
          * @return This {@code Builder}.
          */
         @NonNull
-        @RequiresVendorApiLevel(level = 5)
-        public Builder setAnimationBackground(@NonNull AnimationBackground background) {
-            mAnimationBackground = background;
+        @RequiresVendorApiLevel(level = 7)
+        public Builder setAnimationParams(@NonNull AnimationParams params) {
+            mAnimationParams = params;
             return this;
         }
 
@@ -547,17 +599,25 @@
             return this;
         }
 
+        /** Sets the {@link DividerAttributes}. If {@code null}, no divider is requested. */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public Builder setDividerAttributes(@Nullable DividerAttributes dividerAttributes) {
+            mDividerAttributes = dividerAttributes;
+            return this;
+        }
+
         /**
          * Builds a {@link SplitAttributes} instance with the attributes
          * specified by {@link #setSplitType}, {@link #setLayoutDirection}, and
-         * {@link #setAnimationBackground}.
+         * {@link #setAnimationParams}.
          *
          * @return The new {@code SplitAttributes} instance.
          */
         @NonNull
         public SplitAttributes build() {
-            return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackground,
-                    mWindowAttributes);
+            return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationParams,
+                    mWindowAttributes, mDividerAttributes);
         }
     }
 
@@ -567,13 +627,15 @@
         if (!(o instanceof SplitAttributes)) return false;
         SplitAttributes that = (SplitAttributes) o;
         return mLayoutDirection == that.mLayoutDirection && mSplitType.equals(that.mSplitType)
-                && mAnimationBackground.equals(that.mAnimationBackground)
-                && mWindowAttributes.equals(that.mWindowAttributes);
+                && Objects.equals(mAnimationParams, that.mAnimationParams)
+                && mWindowAttributes.equals(that.mWindowAttributes)
+                && Objects.equals(mDividerAttributes, that.mDividerAttributes);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mLayoutDirection, mSplitType, mAnimationBackground, mWindowAttributes);
+        return Objects.hash(mLayoutDirection, mSplitType, mAnimationParams, mWindowAttributes,
+                mDividerAttributes);
     }
 
     @NonNull
@@ -582,8 +644,9 @@
         return SplitAttributes.class.getSimpleName() + "{"
                 + "layoutDir=" + layoutDirectionToString()
                 + ", splitType=" + mSplitType
-                + ", animationBackground=" + mAnimationBackground
+                + ", animationParams=" + mAnimationParams
                 + ", windowAttributes=" + mWindowAttributes
+                + ", dividerAttributes=" + mDividerAttributes
                 + "}";
     }
 
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
new file mode 100644
index 0000000..17bb15a
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.RequiresVendorApiLevel;
+import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.util.SetUtilApi23;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a fold on a display that may intersect a window. The presence of a fold does not
+ * imply that it intersects the window an {@link android.app.Activity} is running in. For
+ * example, on a device that can fold like a book and has an outer screen, the fold should be
+ * reported regardless of the folding state, or which screen is on to indicate that there may
+ * be a fold when the user opens the device.
+ *
+ * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to listen to features
+ * that affect the window.
+ */
+public final class DisplayFoldFeature {
+
+    /**
+     * The type of fold is unknown. This is here for compatibility reasons if a new type is added,
+     * and cannot be reported to an incompatible application.
+     */
+    public static final int TYPE_UNKNOWN = 0;
+
+    /**
+     * The type of fold is a physical hinge separating two display panels.
+     */
+    public static final int TYPE_HINGE = 1;
+
+    /**
+     * The type of fold is a screen that folds from 0-180.
+     */
+    public static final int TYPE_SCREEN_FOLD_IN = 2;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {TYPE_UNKNOWN, TYPE_HINGE, TYPE_SCREEN_FOLD_IN})
+    public @interface FoldType {
+    }
+
+    /**
+     * The fold supports the half opened state.
+     */
+    public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1;
+
+    @Target(ElementType.TYPE_USE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {FOLD_PROPERTY_SUPPORTS_HALF_OPENED})
+    public @interface FoldProperty {
+    }
+
+    @FoldType
+    private final int mType;
+
+    private final Set<@FoldProperty Integer> mProperties;
+
+    /**
+     * Creates an instance of [FoldDisplayFeature].
+     *
+     * @param type                  the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or
+     *                              [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]
+     */
+    DisplayFoldFeature(@FoldType int type, @NonNull Set<@FoldProperty Integer> properties) {
+        mType = type;
+        if (Build.VERSION.SDK_INT >= 23) {
+            mProperties = SetUtilApi23.createSet();
+        } else {
+            mProperties = new HashSet<>();
+        }
+        mProperties.addAll(properties);
+    }
+
+    /**
+     * Returns the type of fold that is either a hinge or a fold.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @FoldType
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns {@code true} if the fold has the given property, {@code false} otherwise.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public boolean hasProperty(@FoldProperty int property) {
+        return mProperties.contains(property);
+    }
+    /**
+     * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    public boolean hasProperties(@NonNull @FoldProperty int... properties) {
+        for (int i = 0; i < properties.length; i++) {
+            if (!mProperties.contains(properties[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DisplayFoldFeature that = (DisplayFoldFeature) o;
+        return mType == that.mType && Objects.equals(mProperties, that.mProperties);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mProperties);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return "ScreenFoldDisplayFeature{mType=" + mType + ", mProperties=" + mProperties + '}';
+    }
+
+    /**
+     * A builder to construct an instance of {@link DisplayFoldFeature}.
+     */
+    public static final class Builder {
+
+        @FoldType
+        private int mType;
+
+        private Set<@FoldProperty Integer> mProperties;
+
+        /**
+         * Constructs a builder to create an instance of {@link DisplayFoldFeature}.
+         *
+         * @param type                  the type of hinge for the {@link DisplayFoldFeature}.
+         * @see DisplayFoldFeature.FoldType
+         */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder(@FoldType int type) {
+            mType = type;
+            if (Build.VERSION.SDK_INT >= 23) {
+                mProperties = SetUtilApi23.createSet();
+            } else {
+                mProperties = new HashSet<>();
+            }
+        }
+
+        /**
+         * Add a property to the set of properties exposed by {@link DisplayFoldFeature}.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        @RequiresVendorApiLevel(level = 6)
+        public Builder addProperty(@FoldProperty int property) {
+            mProperties.add(property);
+            return this;
+        }
+
+        /**
+         * Add a list of properties to the set of properties exposed by
+         * {@link DisplayFoldFeature}.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        @RequiresVendorApiLevel(level = 6)
+        public Builder addProperties(@NonNull @FoldProperty int... properties) {
+            for (int i = 0; i < properties.length; i++) {
+                mProperties.add(properties[i]);
+            }
+            return this;
+        }
+
+        /**
+         * Clear the properties in the builder.
+         */
+        @NonNull
+        @RequiresVendorApiLevel(level = 6)
+        public Builder clearProperties() {
+            mProperties.clear();
+            return this;
+        }
+
+        /**
+         * Returns an instance of {@link DisplayFoldFeature}.
+         */
+        @RequiresVendorApiLevel(level = 6)
+        @NonNull
+        public DisplayFoldFeature build() {
+            return new DisplayFoldFeature(mType, mProperties);
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
new file mode 100644
index 0000000..2143e29
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.RequiresVendorApiLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to represent all the possible features that may interact with or appear in a window,
+ * that an application might want to respond to.
+ */
+public final class SupportedWindowFeatures {
+
+    private final List<DisplayFoldFeature> mDisplayFoldFeatureList;
+
+    private SupportedWindowFeatures(
+            @NonNull List<DisplayFoldFeature> displayFoldFeatureList) {
+        mDisplayFoldFeatureList = new ArrayList<>(displayFoldFeatureList);
+    }
+
+    /**
+     * Returns the possible {@link DisplayFoldFeature}s that can be reported to an application.
+     */
+    @NonNull
+    @RequiresVendorApiLevel(level = 6)
+    public List<DisplayFoldFeature> getDisplayFoldFeatures() {
+        return new ArrayList<>(mDisplayFoldFeatureList);
+    }
+
+
+    /**
+     * A class to create a {@link SupportedWindowFeatures} instance.
+     */
+    public static final class Builder {
+
+        private final List<DisplayFoldFeature> mDisplayFoldFeatures;
+
+        /**
+         * Creates a new instance of {@link Builder}
+         */
+        @RequiresVendorApiLevel(level = 6)
+        public Builder(@NonNull List<DisplayFoldFeature> displayFoldFeatures) {
+            mDisplayFoldFeatures = new ArrayList<>(displayFoldFeatures);
+        }
+
+        /**
+         * Creates an instance of {@link SupportedWindowFeatures} with the features set.
+         */
+        @NonNull
+        @RequiresVendorApiLevel(level = 6)
+        public SupportedWindowFeatures build() {
+            return new SupportedWindowFeatures(mDisplayFoldFeatures);
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
index 875f763..d55e341 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
@@ -96,4 +96,17 @@
         throw new UnsupportedOperationException("This method must not be called unless there is a"
                 + " corresponding override implementation on the device.");
     }
+
+    /**
+     * Returns the {@link SupportedWindowFeatures} for the device. This value will not change
+     * over time.
+     * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to register a
+     * listener for features that impact the window.
+     */
+    @RequiresVendorApiLevel(level = 6)
+    @NonNull
+    default SupportedWindowFeatures getSupportedWindowFeatures() {
+        throw new UnsupportedOperationException("This method will not be called unless there is a"
+                + " corresponding override implementation on the device");
+    }
 }
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
new file mode 100644
index 0000000..69d0784
--- /dev/null
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding;
+
+import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_DRAGGABLE;
+import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_FIXED;
+import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT;
+import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Verifies {@link DividerAttributes} behavior. */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class DividerAttributesTest {
+
+    @Test
+    public void testDividerAttributesDefaults() {
+        final DividerAttributes defaultAttrs =
+                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED).build();
+        assertThat(defaultAttrs.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
+        assertThat(defaultAttrs.getWidthDp()).isEqualTo(WIDTH_SYSTEM_DEFAULT);
+        assertThat(defaultAttrs.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+        assertThat(defaultAttrs.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+    }
+
+    @Test
+    public void testDividerAttributesBuilder() {
+        final DividerAttributes dividerAttributes1 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+                        .setWidthDp(20)
+                        .build();
+        assertThat(dividerAttributes1.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
+        assertThat(dividerAttributes1.getWidthDp()).isEqualTo(20);
+        assertThat(dividerAttributes1.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+        assertThat(dividerAttributes1.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+        final DividerAttributes dividerAttributes2 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .setPrimaryMinRatio(0.2f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+        assertThat(dividerAttributes2.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+        assertThat(dividerAttributes2.getWidthDp()).isEqualTo(20);
+        assertThat(dividerAttributes2.getPrimaryMinRatio()).isEqualTo(0.2f);
+        assertThat(dividerAttributes2.getPrimaryMaxRatio()).isEqualTo(0.8f);
+
+        final DividerAttributes dividerAttributes3 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .build();
+        assertThat(dividerAttributes3.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+        assertThat(dividerAttributes3.getWidthDp()).isEqualTo(20);
+        assertThat(dividerAttributes3.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+        assertThat(dividerAttributes3.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+        final DividerAttributes dividerAttributes4 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .setPrimaryMinRatio(0.2f)
+                        .build();
+        assertThat(dividerAttributes4.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+        assertThat(dividerAttributes4.getWidthDp()).isEqualTo(20);
+        assertThat(dividerAttributes4.getPrimaryMinRatio()).isEqualTo(0.2f);
+        assertThat(dividerAttributes4.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+
+        final DividerAttributes dividerAttributes5 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .setPrimaryMaxRatio(0.2f)
+                        .build();
+        assertThat(dividerAttributes5.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
+        assertThat(dividerAttributes5.getWidthDp()).isEqualTo(20);
+        assertThat(dividerAttributes5.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
+        assertThat(dividerAttributes5.getPrimaryMaxRatio()).isEqualTo(0.2f);
+    }
+
+    @Test
+    public void testDividerAttributesEquals() {
+        final DividerAttributes dividerAttributes1 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .setPrimaryMinRatio(0.2f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+
+        final DividerAttributes dividerAttributes2 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(20)
+                        .setPrimaryMinRatio(0.2f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+
+        final DividerAttributes dividerAttributes3 =
+                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+                        .setWidthDp(20)
+                        .build();
+
+        assertThat(dividerAttributes1).isEqualTo(dividerAttributes2);
+        assertThat(dividerAttributes1).isNotEqualTo(dividerAttributes3);
+    }
+
+    @Test
+    public void testDividerAttributesValidation() {
+        assertThrows(
+                "Must not set min max ratio for DIVIDER_TYPE_FIXED",
+                IllegalStateException.class,
+                () -> new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
+                        .setPrimaryMinRatio(0.2f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build()
+        );
+
+        assertThrows(
+                "Min ratio must be less than or equal to max ratio",
+                IllegalStateException.class,
+                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMinRatio(0.8f)
+                        .setPrimaryMaxRatio(0.2f)
+                        .build()
+        );
+
+        assertThrows(
+                "Min ratio must be in range [0.0, 1.0] or RATIO_UNSET",
+                IllegalArgumentException.class,
+                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMinRatio(2.0f)
+        );
+
+        assertThrows(
+                "Max ratio must be in range [0.0, 1.0] or RATIO_UNSET",
+                IllegalArgumentException.class,
+                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMaxRatio(2.0f)
+        );
+
+        assertThrows(
+                "Width must be greater than or equal to zero or WIDTH_UNSET",
+                IllegalArgumentException.class,
+                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(-10)
+        );
+    }
+}
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
index 1c998a0..7bffb2b 100644
--- a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
@@ -36,30 +36,51 @@
 public class SplitAttributesTest {
     @Test
     public void testSplitAttributesEquals() {
+        final AnimationParams animParams1 =
+                new AnimationParams.Builder()
+                        .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                        .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+                        .build();
         final SplitAttributes attrs1 = new SplitAttributes.Builder()
                 .setSplitType(splitEqually())
                 .setLayoutDirection(LayoutDirection.LOCALE)
-                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                .setAnimationParams(animParams1)
                 .build();
+        final AnimationParams animParams2 =
+                new AnimationParams.Builder()
+                        .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                        .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+                        .build();
         final SplitAttributes attrs2 = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                 .setLayoutDirection(LayoutDirection.LOCALE)
-                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                .setAnimationParams(animParams2)
                 .build();
+        final AnimationParams animParams3 =
+                new AnimationParams.Builder()
+                        .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                        .setOpenAnimationResId(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID)
+                        .build();
         final SplitAttributes attrs3 = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                 .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
-                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
+                .setAnimationParams(animParams3)
                 .build();
+        final AnimationParams animParams4and5 =
+                new AnimationParams.Builder()
+                        .setAnimationBackground(
+                                AnimationBackground.createColorBackground(Color.BLUE))
+                        .setOpenAnimationResId(android.R.anim.fade_in)
+                        .build();
         final SplitAttributes attrs4 = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                 .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
-                .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
+                .setAnimationParams(animParams4and5)
                 .build();
         final SplitAttributes attrs5 = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                 .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
-                .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
+                .setAnimationParams(animParams4and5)
                 .build();
 
         assertNotEquals(attrs1, attrs2);
@@ -79,6 +100,18 @@
     }
 
     @Test
+    public void testSplitAttributesEqualsUsingBuilderFromExistingInstance() {
+        final AnimationParams animParamsDefault = new AnimationParams.Builder().build();
+        final SplitAttributes attrs1 = new SplitAttributes.Builder()
+                .setSplitType(splitEqually())
+                .setLayoutDirection(LayoutDirection.LOCALE)
+                .setAnimationParams(animParamsDefault)
+                .build();
+        final SplitAttributes attrs2 = new SplitAttributes.Builder(attrs1).build();
+        assertEquals(attrs1, attrs2);
+    }
+
+    @Test
     public void testSplitTypeEquals() {
         final SplitAttributes.SplitType[] splitTypes = new SplitAttributes.SplitType[]{
                 new SplitAttributes.SplitType.ExpandContainersSplitType(),
diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
new file mode 100644
index 0000000..e9937a0
--- /dev/null
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout;
+
+import static androidx.window.extensions.layout.DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED;
+import static androidx.window.extensions.layout.DisplayFoldFeature.TYPE_HINGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DisplayFoldFeatureTest {
+
+    @Test
+    public void test_builder_matches_constructor() {
+        Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
+        properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
+        DisplayFoldFeature expected = new DisplayFoldFeature(TYPE_HINGE, properties);
+
+        DisplayFoldFeature actual = new DisplayFoldFeature.Builder(TYPE_HINGE)
+                .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+                .build();
+
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void test_equals_matches() {
+        Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
+        properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
+        DisplayFoldFeature first = new DisplayFoldFeature(TYPE_HINGE, properties);
+        DisplayFoldFeature second = new DisplayFoldFeature(TYPE_HINGE, properties);
+
+        assertEquals(first, second);
+        assertEquals(first.hashCode(), second.hashCode());
+    }
+
+    @Test
+    public void test_getter_matches_values() {
+        final int type = TYPE_HINGE;
+        DisplayFoldFeature actual = new DisplayFoldFeature.Builder(type)
+                .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+                .build();
+
+        assertEquals(type, actual.getType());
+        assertTrue(actual.hasProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
+        assertTrue(actual.hasProperties(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
+    }
+}
diff --git a/window/integration-tests/macrobenchmark-target/build.gradle b/window/integration-tests/macrobenchmark-target/build.gradle
index 8bec782..1631c90 100644
--- a/window/integration-tests/macrobenchmark-target/build.gradle
+++ b/window/integration-tests/macrobenchmark-target/build.gradle
@@ -21,6 +21,7 @@
 }
 
 android {
+    compileSdkVersion 35
     defaultConfig {
         minSdk 28
     }
diff --git a/window/window-demos/demo-common/build.gradle b/window/window-demos/demo-common/build.gradle
index a828dd0..27c4c9c 100644
--- a/window/window-demos/demo-common/build.gradle
+++ b/window/window-demos/demo-common/build.gradle
@@ -16,13 +16,16 @@
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
     id("com.android.library")
     id("kotlin-android")
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         minSdkVersion 23
+        targetSdkVersion 35
     }
     buildFeatures {
         viewBinding true
@@ -31,9 +34,9 @@
 }
 
 dependencies {
-    implementation("androidx.appcompat:appcompat:1.2.0")
+    implementation(project(":appcompat:appcompat"))
     implementation("androidx.core:core-ktx:1.3.2")
-    implementation("androidx.activity:activity:1.2.0")
+    implementation("androidx.activity:activity:1.9.0")
     implementation("androidx.recyclerview:recyclerview:1.2.1")
     api(libs.constraintLayout)
     // TODO(b/152245564) Conflicting dependencies cause IDE errors.
@@ -42,6 +45,11 @@
     implementation("androidx.browser:browser:1.3.0")
     implementation("androidx.startup:startup-runtime:1.1.0")
 
+    // Compose dependencies
+    implementation(project(":compose:runtime:runtime"))
+    implementation(project(":compose:foundation:foundation"))
+    implementation("androidx.compose.material3:material3:1.2.1")
+
     implementation(project(":window:window-java"))
     debugImplementation(libs.leakcanary)
 }
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt
new file mode 100644
index 0000000..686c802
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DemoTheme.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.common
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.colorResource
+
+@Composable
+fun DemoTheme(
+    darkTheme: Boolean = isSystemInDarkTheme(),
+    content: @Composable () -> Unit,
+) {
+    val colorScheme =
+        if (darkTheme) {
+            darkColorScheme(
+                primary = colorResource(R.color.colorPrimaryDark),
+                secondary = colorResource(R.color.colorAccent),
+            )
+        } else {
+            lightColorScheme(
+                primary = colorResource(R.color.colorPrimary),
+                secondary = colorResource(R.color.colorAccent),
+            )
+        }
+
+    MaterialTheme(colorScheme = colorScheme, content = content)
+}
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
index fee0c73..bef6cf6 100644
--- a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
@@ -21,7 +21,6 @@
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
@@ -40,7 +39,7 @@
 import kotlinx.coroutines.launch
 
 /** Demo activity that shows all display features and current device state on the screen. */
-open class DisplayFeaturesActivity : AppCompatActivity() {
+open class DisplayFeaturesActivity : EdgeToEdgeActivity() {
 
     private val infoLogAdapter = InfoLogAdapter()
     private val displayFeatureViews = ArrayList<View>()
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt
new file mode 100644
index 0000000..06bebf78
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/EdgeToEdgeActivity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.common
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+
+/** An activity to make its UI content edge-to-edge and fits system windows. */
+open class EdgeToEdgeActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        enableEdgeToEdge()
+        super.onCreate(savedInstanceState)
+    }
+
+    override fun setContentView(view: View?) {
+        super.setContentView(view)
+        view?.fitsSystemWindows = true
+    }
+}
diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
index 454d6fe..5963f3df 100644
--- a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
@@ -53,6 +53,11 @@
         ++id
     }
 
+    fun appendAndNotify(title: String, message: String) {
+        append(title, message)
+        notifyDataSetChanged()
+    }
+
     private fun append(item: InfoLog) {
         items.add(0, item)
     }
diff --git a/window/window-demos/demo-second-app/build.gradle b/window/window-demos/demo-second-app/build.gradle
index 336950f..c39a226 100644
--- a/window/window-demos/demo-second-app/build.gradle
+++ b/window/window-demos/demo-second-app/build.gradle
@@ -21,9 +21,11 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         applicationId "androidx.window.demo2"
         minSdkVersion 23
+        targetSdkVersion 35
     }
     buildFeatures {
         viewBinding true
@@ -32,8 +34,8 @@
 }
 
 dependencies {
-    implementation("androidx.activity:activity:1.2.0")
-    implementation("androidx.appcompat:appcompat:1.2.0")
+    implementation("androidx.activity:activity:1.9.0")
+    implementation(project(":appcompat:appcompat"))
     api(libs.constraintLayout)
     implementation("androidx.core:core-ktx:1.8.0")
     // TODO(b/152245564) Conflicting dependencies cause IDE errors.
diff --git a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
index 15d5101..177762b 100644
--- a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
         android:supportsRtl="true">
         <activity
             android:name=".embedding.TrustedEmbeddingActivity"
+            android:supportsPictureInPicture="true"
             android:exported="true"
             android:label="@string/trusted_embedding_activity"
             android:configChanges=
@@ -30,6 +31,7 @@
         </activity>
         <activity
             android:name=".embedding.UntrustedEmbeddingActivity"
+            android:supportsPictureInPicture="true"
             android:exported="true"
             android:label="@string/untrusted_embedding_activity"
             android:configChanges=
@@ -39,6 +41,11 @@
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <!-- Require to restore the split after entering and exiting picture-in-picture mode
+            for untrusted embedding. -->
+            <property
+                android:name="android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING"
+                android:value="true" />
         </activity>
         <activity-alias
             android:name="androidx.window.demo2.DisplayFeaturesActivity"
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
new file mode 100644
index 0000000..e3406ce
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo2.embedding
+
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.os.Bundle
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.common.util.PictureInPictureUtil
+import androidx.window.demo2.R
+import androidx.window.demo2.databinding.ActivityEmbeddedBinding
+import androidx.window.embedding.ActivityEmbeddingController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+open class EmbeddedActivityBase : EdgeToEdgeActivity() {
+    lateinit var viewBinding: ActivityEmbeddedBinding
+    private lateinit var activityEmbeddingController: ActivityEmbeddingController
+    private lateinit var windowInfoView: TextView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        viewBinding = ActivityEmbeddedBinding.inflate(layoutInflater)
+        setContentView(viewBinding.root)
+        viewBinding.buttonPip.setOnClickListener {
+            PictureInPictureUtil.startPictureInPicture(this, false)
+        }
+        viewBinding.buttonStartActivity.setOnClickListener {
+            startActivity(Intent(this, this.javaClass))
+        }
+        viewBinding.buttonStartActivityFromApplicationContext.setOnClickListener {
+            application.startActivity(Intent(this, this.javaClass).setFlags(FLAG_ACTIVITY_NEW_TASK))
+        }
+
+        activityEmbeddingController = ActivityEmbeddingController.getInstance(this)
+        initializeEmbeddedActivityInfoCallback()
+    }
+
+    private fun initializeEmbeddedActivityInfoCallback() {
+        val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
+        if (extensionVersion < 6) {
+            // EmbeddedActivityWindowInfo is only available on 6+.
+            return
+        }
+
+        windowInfoView = viewBinding.windowIntoText
+        lifecycleScope.launch(Dispatchers.Main) {
+            // Collect EmbeddedActivityWindowInfo when STARTED and stop when STOPPED.
+            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // After register, the flow will be triggered immediately if the activity is
+                // embedded.
+                // However, if the activity is changed to non-embedded state in background (after
+                // STOPPED), the flow will not report the change (because it has been unregistered).
+                // Reset before start listening.
+                resetWindowInfoView()
+                activityEmbeddingController
+                    .embeddedActivityWindowInfo(this@EmbeddedActivityBase)
+                    .collect { info ->
+                        if (info.isEmbedded) {
+                            windowInfoView.text = info.toString()
+                        } else {
+                            resetWindowInfoView()
+                        }
+                    }
+            }
+        }
+    }
+
+    private fun resetWindowInfoView() {
+        windowInfoView.text = getString(R.string.embedded_window_info_unavailable)
+    }
+}
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
index 67c0b09..1a2ea23 100644
--- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
@@ -16,9 +16,7 @@
 
 package androidx.window.demo2.embedding
 
-import android.app.Activity
 import android.os.Bundle
-import android.widget.TextView
 import androidx.window.demo2.R
 
 /**
@@ -26,12 +24,10 @@
  * `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from the
  * split demos in window-samples/demos.
  */
-class TrustedEmbeddingActivity : Activity() {
+class TrustedEmbeddingActivity : EmbeddedActivityBase() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_embedded)
-        findViewById<TextView>(R.id.detail_text_view).text =
-            getString(R.string.trusted_embedding_activity_detail)
+        viewBinding.detailTextView.text = getString(R.string.trusted_embedding_activity_detail)
     }
 }
diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
index b8405f4..487e4fa 100644
--- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
@@ -16,21 +16,17 @@
 
 package androidx.window.demo2.embedding
 
-import android.app.Activity
 import android.os.Bundle
-import android.widget.TextView
 import androidx.window.demo2.R
 
 /**
  * Activity that can be embedded in untrusted mode. See `android:allowUntrustedActivityEmbedding` in
  * AndroidManifest. Activity can be launched from the split demos in window-samples/demos.
  */
-class UntrustedEmbeddingActivity : Activity() {
+class UntrustedEmbeddingActivity : EmbeddedActivityBase() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_embedded)
-        findViewById<TextView>(R.id.detail_text_view).text =
-            getString(R.string.untrusted_embedding_activity_detail)
+        viewBinding.detailTextView.text = getString(R.string.untrusted_embedding_activity_detail)
     }
 }
diff --git a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
index fbd572c..102145e 100644
--- a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
+++ b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
@@ -15,9 +15,13 @@
   limitations under the License.
   -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="10dp"
+    android:background="@color/colorAccent">
 
     <TextView
         android:layout_gravity="center"
@@ -25,4 +29,44 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 
-</FrameLayout>
\ No newline at end of file
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
+        android:background="#AAAAAA" />
+
+    <TextView
+        android:layout_gravity="center"
+        android:id="@+id/window_into_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/embedded_window_info_unavailable"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
+        android:background="#AAAAAA" />
+
+    <Button
+        android:id="@+id/button_pip"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="ENTER PIP" />
+    <Button
+        android:id="@+id/button_start_activity"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="Start a new instance of current Activity" />
+    <Button
+        android:id="@+id/button_start_activity_from_application_context"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:text="Start a new instance of current Activity from Application Context" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo-second-app/src/main/res/values/strings.xml b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
index de4172b..7fa3d6d 100644
--- a/window/window-demos/demo-second-app/src/main/res/values/strings.xml
+++ b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
@@ -22,4 +22,6 @@
     <string name="untrusted_embedding_activity">Untrusted Embedding Activity</string>
     <string name="untrusted_embedding_activity_detail">Activity allows embedding in untrusted mode
         via opt-in.</string>
+    <string name="embedded_window_info_unavailable">EmbeddedActivityWindowInfo not available
+    </string>
 </resources>
\ No newline at end of file
diff --git a/window/window-demos/demo/build.gradle b/window/window-demos/demo/build.gradle
index 615fea8..fc2d898 100644
--- a/window/window-demos/demo/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -21,19 +21,22 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.LibraryType
+
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
     id("com.android.application")
     id("org.jetbrains.kotlin.android")
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         applicationId "androidx.window.demo"
         minSdkVersion 23
+        targetSdkVersion 35
     }
     buildFeatures {
         viewBinding true
@@ -65,9 +68,9 @@
 }
 
 dependencies {
-    implementation("androidx.appcompat:appcompat:1.5.1")
+    implementation(project(":appcompat:appcompat"))
     implementation("androidx.core:core-ktx:1.3.2")
-    implementation("androidx.activity:activity:1.2.0")
+    implementation("androidx.activity:activity:1.9.0")
     implementation("androidx.recyclerview:recyclerview:1.2.1")
     // TODO(b/262583150): force tracing 1.1.0 since its required by androidTest
     implementation("androidx.tracing:tracing:1.1.0")
@@ -77,8 +80,20 @@
     implementation "androidx.browser:browser:1.3.0"
     implementation("androidx.startup:startup-runtime:1.1.0")
 
+    // Compose dependencies
+    implementation(project(":compose:runtime:runtime"))
+    implementation(project(":compose:foundation:foundation"))
+    implementation(project(":compose:foundation:foundation-layout"))
+    implementation(project(":compose:ui:ui-tooling-preview"))
+    debugImplementation(project(":compose:ui:ui-tooling"))
+    implementation("androidx.activity:activity-compose:1.9.0")
+    implementation("androidx.compose.material3:material3:1.2.1")
+    implementation("androidx.compose.material:material-icons-extended:1.6.8")
+    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
+
     implementation(project(":window:window-java"))
     implementation(project(":window:window-demos:demo-common"))
+
     debugImplementation(libs.leakcanary)
 
     androidTestImplementation(libs.testCore)
@@ -89,7 +104,7 @@
     androidTestImplementation(project(":window:window-testing"))
     androidTestImplementation(project(":window:window-demos:demo-common"))
 
-    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2")
+    coreLibraryDesugaring(libs.desugarJdkLibs)
 }
 
 // Allow usage of Kotlin's @OptIn.
diff --git a/window/window-demos/demo/src/main/AndroidManifest.xml b/window/window-demos/demo/src/main/AndroidManifest.xml
index 5b81535..214ca2d 100644
--- a/window/window-demos/demo/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo/src/main/AndroidManifest.xml
@@ -22,18 +22,12 @@
         <property
             android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
             android:value="true" />
+        <!-- The app itself supports activity embedding, so a system override is not needed. -->
+        <property
+            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
+            android:value="false" />
 
-        <service android:name=".TestIme"
-            android:label="@string/test_ime"
-            android:permission="android.permission.BIND_INPUT_METHOD"
-            android:exported="true">
-            <intent-filter>
-                <action android:name="android.view.InputMethod"/>
-            </intent-filter>
-            <meta-data android:name="android.view.im"
-                android:resource="@xml/method"/>
-        </service>
-
+        <!--region WindowManager Demos -->
         <activity android:name=".demos.WindowDemosActivity"
             android:exported="true"
             android:label="@string/windowManagerDemos">
@@ -74,6 +68,9 @@
             android:exported="false"
             android:configChanges="orientation|screenSize|screenLayout|screenSize"
             android:label="@string/window_metrics"/>
+        <!--endregion WindowManager Demos -->
+
+        <!--region Rear Display Demos -->
         <activity android:name=".area.RearDisplayActivityConfigChanges"
             android:exported="true"
             android:configChanges=
@@ -84,6 +81,13 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity android:name=".area.RearDisplayPresentationActivity"
+            android:exported="false"
+            android:configChanges="orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:label="@string/dual_display" />
+        <!--endregion Rear Display Demos -->
+
+        <!--region Sample showcase of split activity rules -->
         <activity
             android:name=".embedding.SplitActivityA"
             android:exported="true"
@@ -132,10 +136,17 @@
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
+            android:name=".embedding.DialogActivity"
+            android:theme="@style/Theme.AppCompat.Dialog"
+            android:exported="false"
+            android:label="Dialog Activity"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
+        <activity
             android:name=".embedding.ExpandedDialogActivity"
             android:theme="@style/ExpandedDialogTheme"
             android:exported="false"
-            android:label="Dialog Activity"
+            android:label="Expanded Dialog Activity"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
@@ -176,9 +187,9 @@
             android:label="Placeholder"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.list_detail_split_affinity" />
+        <!--endregion Sample showcase of split activity rules -->
 
-        <!-- Split PiP App -->
-
+        <!-- region Split PiP App -->
         <activity
             android:name=".embedding.SplitPipActivityA"
             android:exported="true"
@@ -213,10 +224,11 @@
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.split_pip">
         </activity>
+        <!-- endregion Split PiP App -->
 
+        <!-- region Split on Device State -->
         <!-- The demo App to show how to change the current split layout with the current device and
          window states -->
-
         <activity
             android:name=".embedding.SplitDeviceStateActivityA"
             android:exported="true"
@@ -235,7 +247,9 @@
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.split_device_state_activity_affinity">
         </activity>
+        <!-- endregion Split on Device State -->
 
+        <!-- region Split Toggle at Runtime -->
         <!-- The demo app to show how to change layout with runtime APIs -->
         <activity
             android:name=".embedding.SplitAttributesToggleMainActivity"
@@ -263,16 +277,26 @@
             android:label="Split Toggle Placeholder"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.split_attributes_toggle_activity_affinity" />
+        <!-- endregion Split Toggle at Runtime -->
 
+        <!-- region IME usages in ActivityEmbedding split -->
         <!-- The demo app that shows various IME-related use cases -->
-
         <activity android:name=".ImeActivity"
             android:exported="false"
             android:configChanges="orientation|screenSize|screenLayout|screenSize"
-            android:label="@string/ime"/>
-
+            android:label="@string/ime" />
+        <service android:name=".TestIme"
+            android:label="@string/test_ime"
+            android:permission="android.permission.BIND_INPUT_METHOD"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod" />
+            </intent-filter>
+            <meta-data
+                android:name="android.view.im"
+                android:resource="@xml/method" />
+        </service>
         <!-- The demo app to show IME usages in ActivityEmbedding split. -->
-
         <activity
             android:name=".embedding.SplitImeActivityMain"
             android:exported="true"
@@ -291,9 +315,37 @@
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:taskAffinity="androidx.window.demo.split_ime">
         </activity>
+        <!-- endregion IME usages in ActivityEmbedding split -->
 
+        <!-- region Overlay features -->
+        <!-- The demo app to show how to use overlay features -->
+        <activity
+            android:name=".embedding.OverlayAssociatedActivityA"
+            android:exported="true"
+            android:label="Overlay Activity A"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".embedding.OverlayAssociatedActivityB"
+            android:exported="true"
+            android:label="Overlay Activity B"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
+        </activity>
+        <activity-alias
+            android:name=".embedding.SplitWithOverlayActivity"
+            android:targetActivity=".embedding.SplitActivityDetail"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.overlay_activity_affinity" />
+        <!-- endregion Overlay features -->
+
+        <!-- region Display configuration callbacks -->
         <!-- The demo app to show display configuration from different system callbacks. -->
-
         <activity
             android:name=".coresdk.WindowStateCallbackActivity"
             android:exported="true"
@@ -305,9 +357,9 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <!-- endregion Display configuration callbacks -->
 
-        <!-- Activity embedding initializer -->
-
+        <!-- region Activity embedding initializer -->
         <provider android:name="androidx.startup.InitializationProvider"
             android:authorities="${applicationId}.androidx-startup"
             android:exported="false"
@@ -316,11 +368,7 @@
             <meta-data  android:name="androidx.window.demo.embedding.ExampleWindowInitializer"
                 android:value="androidx.startup" />
         </provider>
-
-        <!-- The app itself supports activity embedding, so a system override is not needed. -->
-        <property
-            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
-            android:value="false" />
+        <!-- endregion Activity embedding initializer -->
 
     </application>
 </manifest>
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
index 898ce04..f639f70 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
@@ -21,10 +21,10 @@
 import android.provider.Settings
 import android.view.inputmethod.InputMethodManager
 import android.widget.Button
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
 
 /** Demo app that shows various IME-related features. */
-class ImeActivity : AppCompatActivity() {
+class ImeActivity : EdgeToEdgeActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
index 51b4af7..1699741 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
@@ -27,10 +27,10 @@
 import android.view.View
 import android.widget.TextView
 import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
@@ -41,7 +41,7 @@
  * Demo activity that reacts to foldable device state change and shows content on the outside
  * display when the device is folded.
  */
-class PresentationActivity : AppCompatActivity() {
+class PresentationActivity : EdgeToEdgeActivity() {
     private val TAG = "FoldablePresentation"
 
     private var presentation: DemoPresentation? = null
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
index e3b89a2..cf29e6d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
@@ -17,16 +17,16 @@
 package androidx.window.demo
 
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.layout.WindowInfoTracker
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 
 /** Demo of [SplitLayout]. */
-class SplitLayoutActivity : AppCompatActivity() {
+class SplitLayoutActivity : EdgeToEdgeActivity() {
 
     private lateinit var splitLayout: SplitLayout
 
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
index 2a13682..06c94dc 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
@@ -78,7 +78,8 @@
                 .append(
                     "Width: $width, Height: $height\n" +
                         "Top: ${windowMetrics.bounds.top}, Bottom: ${windowMetrics.bounds.bottom}, " +
-                        "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}"
+                        "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}\n" +
+                        "Density: ${windowMetrics.density}"
                 )
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
index 4bdc573..8afa7d5 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
@@ -18,12 +18,12 @@
 
 import android.content.res.Configuration
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.RecyclerView
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.common.infolog.InfoLogAdapter
 import androidx.window.layout.WindowMetricsCalculator
 
-class WindowMetricsActivity : AppCompatActivity() {
+class WindowMetricsActivity : EdgeToEdgeActivity() {
 
     private val adapter = InfoLogAdapter()
 
@@ -45,7 +45,10 @@
         val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
         val width = windowMetrics.bounds.width()
         val height = windowMetrics.bounds.height()
-        adapter.append("WindowMetrics update", "width: $width, height: $height")
+        adapter.append(
+            "WindowMetrics update",
+            "width: $width, height: $height, " + "density: ${windowMetrics.density}"
+        )
         adapter.notifyDataSetChanged()
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
index bff1424..d0514a1 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
@@ -17,23 +17,20 @@
 package androidx.window.demo.area
 
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.area.WindowAreaCapability
 import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_TRANSFER_ACTIVITY_TO_AREA
-import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNAVAILABLE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
 import androidx.window.area.WindowAreaController
 import androidx.window.area.WindowAreaInfo
-import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
 import androidx.window.area.WindowAreaSession
 import androidx.window.area.WindowAreaSessionCallback
 import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.common.infolog.InfoLogAdapter
 import androidx.window.demo.databinding.ActivityRearDisplayBinding
 import java.text.SimpleDateFormat
@@ -41,9 +38,6 @@
 import java.util.Locale
 import java.util.concurrent.Executor
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
 /**
@@ -53,15 +47,14 @@
  * This Activity overrides configuration changes for simplicity.
  */
 @OptIn(ExperimentalWindowApi::class)
-class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
+class RearDisplayActivityConfigChanges : EdgeToEdgeActivity(), WindowAreaSessionCallback {
 
     private lateinit var windowAreaController: WindowAreaController
     private var rearDisplaySession: WindowAreaSession? = null
-    private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
-    private var rearDisplayStatus: WindowAreaCapability.Status = WINDOW_AREA_STATUS_UNSUPPORTED
     private val infoLogAdapter = InfoLogAdapter()
     private lateinit var binding: ActivityRearDisplayBinding
     private lateinit var executor: Executor
+    private var currentWindowAreaInfo: WindowAreaInfo? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -73,59 +66,77 @@
 
         binding.rearStatusRecyclerView.adapter = infoLogAdapter
 
-        binding.rearDisplayButton.setOnClickListener {
-            if (rearDisplayStatus == WINDOW_AREA_STATUS_ACTIVE) {
-                if (rearDisplaySession == null) {
-                    rearDisplaySession =
-                        rearDisplayWindowAreaInfo?.getActiveSession(
-                            OPERATION_TRANSFER_ACTIVITY_TO_AREA
-                        )
+        lifecycleScope.launch(Dispatchers.Main) {
+            // The block passed to repeatOnLifecycle is executed when the lifecycle
+            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+            // It automatically restarts the block when the lifecycle is STARTED again.
+            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Safely collect from windowInfoRepo when the lifecycle is STARTED
+                // and stops collection when the lifecycle is STOPPED
+                windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
+                    infoLogAdapter.appendAndNotify(
+                        getCurrentTimeString(),
+                        "number of areas: " + windowAreaInfos.size
+                    )
+                    windowAreaInfos.forEach { windowAreaInfo ->
+                        if (windowAreaInfo.type == WindowAreaInfo.Type.TYPE_REAR_FACING) {
+                            currentWindowAreaInfo = windowAreaInfo
+                            val transferCapability =
+                                windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)
+                            infoLogAdapter.append(
+                                getCurrentTimeString(),
+                                transferCapability.status.toString() +
+                                    " : " +
+                                    windowAreaInfo.metrics.toString()
+                            )
+                            updateRearDisplayButton()
+                        }
+                    }
+                    infoLogAdapter.notifyDataSetChanged()
                 }
+            }
+        }
+
+        binding.rearDisplayButton.setOnClickListener {
+            if (rearDisplaySession != null) {
                 rearDisplaySession?.close()
             } else {
-                rearDisplayWindowAreaInfo?.token?.let { token ->
+                currentWindowAreaInfo?.let {
                     windowAreaController.transferActivityToWindowArea(
-                        token = token,
-                        activity = this,
-                        executor = executor,
-                        windowAreaSessionCallback = this
+                        it.token,
+                        this,
+                        executor,
+                        this
                     )
                 }
             }
         }
 
-        lifecycleScope.launch(Dispatchers.Main) {
-            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                windowAreaController.windowAreaInfos
-                    .map { windowAreaInfoList ->
-                        windowAreaInfoList.firstOrNull { windowAreaInfo ->
-                            windowAreaInfo.type == TYPE_REAR_FACING
-                        }
-                    }
-                    .onEach { windowAreaInfo -> rearDisplayWindowAreaInfo = windowAreaInfo }
-                    .map(this@RearDisplayActivityConfigChanges::getRearDisplayStatus)
-                    .distinctUntilChanged()
-                    .collect { status ->
-                        infoLogAdapter.append(getCurrentTimeString(), status.toString())
-                        infoLogAdapter.notifyDataSetChanged()
-                        rearDisplayStatus = status
-                        updateRearDisplayButton()
-                    }
+        binding.rearDisplaySessionButton.setOnClickListener {
+            if (rearDisplaySession == null) {
+                try {
+                    rearDisplaySession =
+                        currentWindowAreaInfo?.getActiveSession(OPERATION_TRANSFER_ACTIVITY_TO_AREA)
+                    updateRearDisplayButton()
+                } catch (e: IllegalStateException) {
+                    infoLogAdapter.appendAndNotify(getCurrentTimeString(), e.toString())
+                }
             }
         }
     }
 
     override fun onSessionStarted(session: WindowAreaSession) {
         rearDisplaySession = session
-        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
-        infoLogAdapter.notifyDataSetChanged()
+        infoLogAdapter.appendAndNotify(
+            getCurrentTimeString(),
+            "RearDisplay Session has been started"
+        )
         updateRearDisplayButton()
     }
 
     override fun onSessionEnded(t: Throwable?) {
         rearDisplaySession = null
-        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
-        infoLogAdapter.notifyDataSetChanged()
+        infoLogAdapter.appendAndNotify(getCurrentTimeString(), "RearDisplay Session has ended")
         updateRearDisplayButton()
     }
 
@@ -135,22 +146,24 @@
             binding.rearDisplayButton.text = "Disable RearDisplay Mode"
             return
         }
-        when (rearDisplayStatus) {
-            WINDOW_AREA_STATUS_UNSUPPORTED -> {
-                binding.rearDisplayButton.isEnabled = false
-                binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
-            }
-            WINDOW_AREA_STATUS_UNAVAILABLE -> {
-                binding.rearDisplayButton.isEnabled = false
-                binding.rearDisplayButton.text = "RearDisplay is not currently available"
-            }
-            WINDOW_AREA_STATUS_AVAILABLE -> {
-                binding.rearDisplayButton.isEnabled = true
-                binding.rearDisplayButton.text = "Enable RearDisplay Mode"
-            }
-            WINDOW_AREA_STATUS_ACTIVE -> {
-                binding.rearDisplayButton.isEnabled = true
-                binding.rearDisplayButton.text = "Disable RearDisplay Mode"
+        currentWindowAreaInfo?.let { windowAreaInfo ->
+            when (windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA).status) {
+                WINDOW_AREA_STATUS_UNSUPPORTED -> {
+                    binding.rearDisplayButton.isEnabled = false
+                    binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+                }
+                WINDOW_AREA_STATUS_UNAVAILABLE -> {
+                    binding.rearDisplayButton.isEnabled = false
+                    binding.rearDisplayButton.text = "RearDisplay is not currently available"
+                }
+                WINDOW_AREA_STATUS_AVAILABLE -> {
+                    binding.rearDisplayButton.isEnabled = true
+                    binding.rearDisplayButton.text = "Enable RearDisplay Mode"
+                }
+                else -> {
+                    binding.rearDisplayButton.isEnabled = false
+                    binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+                }
             }
         }
     }
@@ -161,11 +174,6 @@
         return currentDate.toString()
     }
 
-    private fun getRearDisplayStatus(windowAreaInfo: WindowAreaInfo?): WindowAreaCapability.Status {
-        val status = windowAreaInfo?.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)?.status
-        return status ?: WINDOW_AREA_STATUS_UNSUPPORTED
-    }
-
     private companion object {
         private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
     }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
new file mode 100644
index 0000000..c5e5985
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.area
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.area.WindowAreaCapability
+import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaInfo
+import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
+import androidx.window.area.WindowAreaPresentationSessionCallback
+import androidx.window.area.WindowAreaSessionPresenter
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.common.infolog.InfoLogAdapter
+import androidx.window.demo.databinding.ActivityRearDisplayPresentationBinding
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/**
+ * Demo Activity that showcases listening for the status of the [OPERATION_PRESENT_ON_AREA]
+ * operation on a [WindowAreaInfo] of type [TYPE_REAR_FACING] as well as enabling/disabling a
+ * presentation session on that window area. This Activity implements
+ * [WindowAreaPresentationSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class RearDisplayPresentationActivity :
+    EdgeToEdgeActivity(), WindowAreaPresentationSessionCallback {
+
+    private var activePresentation: WindowAreaSessionPresenter? = null
+    private var currentWindowAreaInfo: WindowAreaInfo? = null
+    private lateinit var windowAreaController: WindowAreaController
+    private val infoLogAdapter = InfoLogAdapter()
+
+    private lateinit var binding: ActivityRearDisplayPresentationBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        windowAreaController = WindowAreaController.getOrCreate()
+
+        binding = ActivityRearDisplayPresentationBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+        binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+        lifecycleScope.launch(Dispatchers.Main) {
+            // The block passed to repeatOnLifecycle is executed when the lifecycle
+            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+            // It automatically restarts the block when the lifecycle is STARTED again.
+            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Safely collect from windowInfoRepo when the lifecycle is STARTED
+                // and stops collection when the lifecycle is STOPPED
+                windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
+                    infoLogAdapter.appendAndNotify(
+                        getCurrentTimeString(),
+                        "number of areas: " + windowAreaInfos.size
+                    )
+                    windowAreaInfos.forEach { windowAreaInfo ->
+                        if (windowAreaInfo.type == TYPE_REAR_FACING) {
+                            currentWindowAreaInfo = windowAreaInfo
+                            val presentCapability =
+                                windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA)
+                            infoLogAdapter.append(
+                                getCurrentTimeString(),
+                                presentCapability.status.toString() +
+                                    " : " +
+                                    windowAreaInfo.metrics.toString()
+                            )
+                            updateRearDisplayPresentationButton()
+                        }
+                    }
+                    infoLogAdapter.notifyDataSetChanged()
+                }
+            }
+        }
+
+        binding.rearDisplayPresentationButton.setOnClickListener {
+            if (activePresentation != null) {
+                activePresentation?.close()
+            } else {
+                currentWindowAreaInfo?.let {
+                    windowAreaController.presentContentOnWindowArea(
+                        it.token,
+                        this@RearDisplayPresentationActivity,
+                        { obj: Runnable -> obj.run() },
+                        this@RearDisplayPresentationActivity
+                    )
+                }
+            }
+        }
+    }
+
+    override fun onSessionStarted(session: WindowAreaSessionPresenter) {
+        infoLogAdapter.appendAndNotify(
+            getCurrentTimeString(),
+            "Presentation session has been started"
+        )
+
+        activePresentation = session
+        val concurrentContext: Context = session.context
+        val contentView =
+            LayoutInflater.from(concurrentContext).inflate(R.layout.concurrent_presentation, null)
+        session.setContentView(contentView)
+        activePresentation = session
+        updateRearDisplayPresentationButton()
+    }
+
+    override fun onContainerVisibilityChanged(isVisible: Boolean) {
+        infoLogAdapter.appendAndNotify(
+            getCurrentTimeString(),
+            "Presentation content is visible: $isVisible"
+        )
+    }
+
+    override fun onSessionEnded(t: Throwable?) {
+        infoLogAdapter.appendAndNotify(
+            getCurrentTimeString(),
+            "Presentation session has been ended"
+        )
+        activePresentation = null
+    }
+
+    private fun updateRearDisplayPresentationButton() {
+        if (activePresentation != null) {
+            binding.rearDisplayPresentationButton.isEnabled = true
+            binding.rearDisplayPresentationButton.text = "Disable rear display presentation"
+            return
+        }
+        when (currentWindowAreaInfo?.getCapability(OPERATION_PRESENT_ON_AREA)?.status) {
+            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
+                binding.rearDisplayPresentationButton.isEnabled = false
+                binding.rearDisplayPresentationButton.text =
+                    "Rear display presentation mode is not supported on this device"
+            }
+            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
+                binding.rearDisplayPresentationButton.isEnabled = false
+                binding.rearDisplayPresentationButton.text =
+                    "Rear display presentation is not currently available"
+            }
+            WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
+                binding.rearDisplayPresentationButton.isEnabled = true
+                binding.rearDisplayPresentationButton.text = "Enable rear display presentation mode"
+            }
+        }
+    }
+
+    private fun getCurrentTimeString(): String {
+        val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+        val currentDate = sdf.format(Date())
+        return currentDate.toString()
+    }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
index 97d7ee7..5e71bee 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateCallbackActivity.kt
@@ -25,17 +25,23 @@
 import android.os.Handler
 import android.os.Looper
 import android.view.Display.DEFAULT_DISPLAY
-import androidx.appcompat.app.AppCompatActivity
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.demo.databinding.ActivityCoresdkWindowStateCallbackLayoutBinding
+import androidx.window.demo.R
+import androidx.window.demo.common.DemoTheme
 import androidx.window.layout.WindowInfoTracker
+import androidx.window.layout.WindowMetricsCalculator
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 
 /** Activity to show display configuration from different system callbacks. */
-class WindowStateCallbackActivity : AppCompatActivity() {
+class WindowStateCallbackActivity : ComponentActivity() {
+    private val viewModel: WindowStateViewModel by viewModels()
 
     /**
      * [DisplayManager]s from `Activity` and `Application` are updated from different resource
@@ -43,15 +49,9 @@
      */
     private lateinit var applicationDisplayManager: DisplayManager
     private lateinit var activityDisplayManager: DisplayManager
+    private lateinit var windowMetricsCalculator: WindowMetricsCalculator
     private lateinit var handler: Handler
 
-    private lateinit var latestUpdateView: WindowStateView
-    private lateinit var applicationDisplayListenerView: WindowStateView
-    private lateinit var activityDisplayListenerView: WindowStateView
-    private lateinit var applicationConfigurationView: WindowStateView
-    private lateinit var activityConfigurationView: WindowStateView
-    private lateinit var displayFeatureView: WindowStateView
-
     /**
      * Runnable to poll configuration every 500ms. To always provide an up-to-date configuration so
      * it can be used to verify the configurations from other callbacks.
@@ -59,7 +59,7 @@
     private val updateWindowStateIfChanged =
         object : Runnable {
             override fun run() {
-                latestUpdateView.onWindowStateCallbackInvoked()
+                provideLatestWindowState()
                 handler.postDelayed(this, 500)
             }
         }
@@ -72,9 +72,10 @@
             override fun onDisplayRemoved(displayId: Int) {}
 
             override fun onDisplayChanged(displayId: Int) {
-                if (displayId == DEFAULT_DISPLAY) {
-                    applicationDisplayListenerView.onWindowStateCallbackInvoked()
+                if (displayId != DEFAULT_DISPLAY) {
+                    return
                 }
+                onWindowStateCallbackInvoked(R.string.application_display_listener_title, displayId)
             }
         }
 
@@ -86,36 +87,38 @@
             override fun onDisplayRemoved(displayId: Int) {}
 
             override fun onDisplayChanged(displayId: Int) {
-                if (displayId == DEFAULT_DISPLAY) {
-                    activityDisplayListenerView.onWindowStateCallbackInvoked()
+                if (displayId != DEFAULT_DISPLAY) {
+                    return
                 }
+                onWindowStateCallbackInvoked(R.string.activity_display_listener_title, displayId)
             }
         }
 
     /** [onConfigurationChanged] on `Application`. */
     private val applicationComponentCallback =
         object : ComponentCallbacks {
-            override fun onConfigurationChanged(p0: Configuration) {
-                applicationConfigurationView.onWindowStateCallbackInvoked()
+            override fun onConfigurationChanged(configuration: Configuration) {
+                onWindowStateCallbackInvoked(
+                    R.string.application_configuration_title,
+                    configuration
+                )
             }
 
+            @Deprecated(
+                "Since API level 34 this is never called. Apps targeting API level 34 " +
+                    "and above may provide an empty implementation."
+            )
             override fun onLowMemory() {}
         }
 
     override fun onCreate(savedInstanceState: Bundle?) {
+        enableEdgeToEdge()
         super.onCreate(savedInstanceState)
-        val viewBinding = ActivityCoresdkWindowStateCallbackLayoutBinding.inflate(layoutInflater)
-        setContentView(viewBinding.root)
-        latestUpdateView = viewBinding.latestUpdateView
-        applicationDisplayListenerView = viewBinding.applicationDisplayListenerView
-        activityDisplayListenerView = viewBinding.activityDisplayListenerView
-        applicationConfigurationView = viewBinding.applicationConfigurationView
-        activityConfigurationView = viewBinding.activityConfigurationView
-        displayFeatureView = viewBinding.displayFeatureView
+        setContent { DemoTheme { WindowStateScreen() } }
 
-        applicationDisplayManager =
-            application.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
-        activityDisplayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        applicationDisplayManager = application.getSystemService(DisplayManager::class.java)
+        activityDisplayManager = getSystemService(DisplayManager::class.java)
+        windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
         handler = Handler(Looper.getMainLooper())
 
         applicationDisplayManager.registerDisplayListener(applicationDisplayListener, handler)
@@ -126,7 +129,9 @@
             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 WindowInfoTracker.getOrCreate(this@WindowStateCallbackActivity)
                     .windowLayoutInfo(this@WindowStateCallbackActivity)
-                    .collect { _ -> displayFeatureView.onWindowStateCallbackInvoked() }
+                    .collect { info ->
+                        onWindowStateCallbackInvoked(R.string.display_feature_title, info)
+                    }
             }
         }
     }
@@ -152,10 +157,35 @@
     /** [onConfigurationChanged] on `Activity`. */
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
-        activityConfigurationView.onWindowStateCallbackInvoked()
+        onWindowStateCallbackInvoked(R.string.activity_configuration_title, newConfig)
     }
 
-    companion object {
-        val TAG = WindowStateCallbackActivity::class.simpleName
+    /** Called when the corresponding system callback is invoked. */
+    private fun onWindowStateCallbackInvoked(resId: Int, details: Any?) {
+        viewModel.onWindowStateCallback(queryWindowState(resId, details = "$details"))
+    }
+
+    private fun provideLatestWindowState() {
+        viewModel.updateLatestWindowState(
+            queryWindowState(
+                R.string.latest_configuration_title,
+                "poll configuration every 500ms",
+            )
+        )
+    }
+
+    private fun queryWindowState(resId: Int, details: String): WindowState {
+        fun DisplayManager.defaultDisplayRotation() = getDisplay(DEFAULT_DISPLAY).rotation
+        fun Context.displayBounds() =
+            windowMetricsCalculator.computeMaximumWindowMetrics(this).bounds
+
+        return WindowState(
+            name = getString(resId),
+            applicationDisplayRotation = applicationDisplayManager.defaultDisplayRotation(),
+            activityDisplayRotation = activityDisplayManager.defaultDisplayRotation(),
+            applicationDisplayBounds = applicationContext.displayBounds(),
+            activityDisplayBounds = [email protected](),
+            callbackDetails = details,
+        )
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt
deleted file mode 100644
index 639491f..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateConfigView.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.demo.coresdk
-
-import android.content.Context
-import android.graphics.Color
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.window.demo.R
-import androidx.window.demo.databinding.WindowStateConfigViewBinding
-
-/** View to show a display configuration value. */
-class WindowStateConfigView
-@JvmOverloads
-constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-    defStyleRes: Int = 0
-) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
-
-    private val configView: TextView
-    private var configValue: String? = null
-
-    /** Whether to highlight the value when it is changed. */
-    var shouldHighlightChange = false
-
-    init {
-        val viewBinding =
-            WindowStateConfigViewBinding.inflate(LayoutInflater.from(context), this, true)
-        configView = viewBinding.configValue
-        context.theme.obtainStyledAttributes(attrs, R.styleable.WindowStateConfigView, 0, 0).apply {
-            try {
-                getString(R.styleable.WindowStateConfigView_configName)?.let {
-                    viewBinding.configName.text = it
-                }
-            } finally {
-                recycle()
-            }
-        }
-    }
-
-    /** Updates the config value. */
-    fun updateValue(value: String) {
-        if (shouldHighlightChange && configValue != null && configValue != value) {
-            // Highlight previous value if changed.
-            configView.setTextColor(Color.RED)
-        } else {
-            configView.setTextColor(Color.BLACK)
-        }
-        configValue = value
-        configView.text = value
-    }
-}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
new file mode 100644
index 0000000..92e85eb
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateScreen.kt
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.coresdk
+
+import android.graphics.Rect
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_180
+import android.view.Surface.ROTATION_270
+import android.view.Surface.ROTATION_90
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.window.demo.R
+import androidx.window.demo.common.DemoTheme
+import java.text.SimpleDateFormat
+import java.util.Date
+import kotlinx.coroutines.launch
+
+/** Composes the main screen for displaying window state information. */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun WindowStateScreen(viewModel: WindowStateViewModel = viewModel()) {
+    val windowStates by viewModel.windowStates.collectAsState()
+    val listState = rememberLazyListState()
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text("Window State Callbacks") },
+                actions = {
+                    IconButton(onClick = viewModel::clearWindowStates) {
+                        Icon(Icons.Filled.Delete, "Clear list")
+                    }
+                },
+                colors =
+                    TopAppBarDefaults.topAppBarColors(
+                        containerColor = MaterialTheme.colorScheme.primary,
+                        titleContentColor = Color.White,
+                        actionIconContentColor = Color.White,
+                    ),
+            )
+        },
+        floatingActionButton = {
+            val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
+            if (showButton) {
+                val coroutineScope = rememberCoroutineScope()
+                FloatingActionButton(
+                    containerColor = MaterialTheme.colorScheme.primary,
+                    contentColor = Color.White,
+                    onClick = { coroutineScope.launch { listState.scrollToItem(0) } },
+                ) {
+                    Text("Back to latest!", modifier = Modifier.padding(8.dp))
+                }
+            }
+        },
+        floatingActionButtonPosition = FabPosition.Center,
+    ) { padding ->
+        WindowStateList(
+            windowStates,
+            padding,
+            listState = listState,
+            onWindowStateItemClick = viewModel::onWindowStateItemClick,
+        )
+    }
+}
+
+/** Previews composable for the [WindowStateScreen] in both light and dark modes. */
+@PreviewLightDark
+@Composable
+fun WindowStateScreenPreview() {
+    val windowStates =
+        listOf(
+            WindowState(
+                name = stringResource(R.string.application_configuration_title),
+                applicationDisplayRotation = ROTATION_270,
+                activityDisplayRotation = ROTATION_270,
+                applicationDisplayBounds = Rect(0, 0, 2208, 1840),
+                activityDisplayBounds = Rect(0, 0, 2208, 1840),
+            ),
+            WindowState(
+                name = stringResource(R.string.activity_display_listener_title),
+                applicationDisplayRotation = ROTATION_270,
+                activityDisplayRotation = ROTATION_270,
+                applicationDisplayBounds = Rect(0, 0, 2208, 1840),
+                activityDisplayBounds = Rect(0, 0, 2208, 1840),
+            ),
+            WindowState(
+                name = stringResource(R.string.display_feature_title),
+                applicationDisplayBounds = Rect(0, 0, 960, 2142),
+                activityDisplayBounds = Rect(0, 0, 960, 2142),
+            ),
+            WindowState(
+                name = stringResource(R.string.latest_configuration_title),
+                applicationDisplayBounds = Rect(0, 0, 960, 2142),
+                activityDisplayBounds = Rect(0, 0, 960, 2142),
+            ),
+        )
+    DemoTheme { WindowStateScreen(viewModel = WindowStateViewModel(windowStates)) }
+}
+
+/**
+ * Composes a scrollable list of [WindowStateCard] items.
+ *
+ * @param windowStates list of [WindowState] objects to display.
+ * @param contentPadding padding to apply to the lazy list.
+ * @param listState state object for the lazy list.
+ * @param onWindowStateItemClick callback when a [WindowState] item is clicked.
+ */
+@Composable
+fun WindowStateList(
+    windowStates: List<WindowState>,
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    listState: LazyListState = rememberLazyListState(),
+    onWindowStateItemClick: (Int) -> Unit,
+) {
+    LazyColumn(
+        contentPadding = contentPadding,
+        state = listState,
+    ) {
+        itemsIndexed(windowStates) { index, state ->
+            WindowStateCard(
+                number = windowStates.size - index,
+                state = state,
+                lastState = windowStates.getOrNull(index + 1) ?: state,
+                toggleExpand = { onWindowStateItemClick(index) },
+                modifier = Modifier.fillMaxWidth(),
+            )
+        }
+    }
+}
+
+/**
+ * Composes a card displaying information about a [WindowState].
+ *
+ * @param number the number to display for this [WindowState].
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ * @param toggleExpand callback to toggle the expanded state of the card.
+ * @param modifier modifier for this card.
+ */
+@Composable
+private fun WindowStateCard(
+    number: Int,
+    state: WindowState,
+    lastState: WindowState,
+    toggleExpand: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val isExpanded = state.isDetailsExpanded
+    Card(modifier.padding(vertical = 4.dp, horizontal = 8.dp)) {
+        Row(
+            modifier =
+                Modifier.padding(horizontal = 8.dp)
+                    .animateContentSize(
+                        animationSpec =
+                            spring(
+                                dampingRatio = Spring.DampingRatioMediumBouncy,
+                                stiffness = Spring.StiffnessLow
+                            )
+                    ),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Row(modifier = Modifier.weight(0.85f), verticalAlignment = Alignment.CenterVertically) {
+                Text("#$number")
+                Spacer(modifier = Modifier.width(6.dp))
+                Column {
+                    if (!isExpanded) {
+                        WindowStateSummary(state, lastState)
+                    } else {
+                        WindowStateDetail(state, lastState)
+                    }
+                }
+            }
+            IconButton(onClick = toggleExpand, modifier = Modifier.weight(0.07f)) {
+                Icon(
+                    if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
+                    contentDescription = if (isExpanded) "Show less" else "Show more",
+                )
+            }
+        }
+    }
+}
+
+/**
+ * Composes a summary view for a collapsed [WindowStateCard].
+ *
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ */
+@Composable
+private fun WindowStateSummary(state: WindowState, lastState: WindowState) {
+    Row(verticalAlignment = Alignment.CenterVertically) {
+        Text(
+            state.name,
+            style = MaterialTheme.typography.bodyMedium,
+            fontWeight = FontWeight.Bold,
+        )
+        Spacer(modifier = Modifier.width(4.dp))
+        Text(
+            state.timeStamp.toSimpleTimeStr(),
+            style = MaterialTheme.typography.bodySmall,
+            overflow = TextOverflow.Ellipsis,
+            maxLines = 1,
+        )
+    }
+    Text(
+        buildAnnotatedString {
+            rotationString(
+                state.applicationDisplayRotation,
+                highlight =
+                    state.applicationDisplayRotation != lastState.applicationDisplayRotation,
+            )
+            append(" / ")
+            rotationString(
+                state.activityDisplayRotation,
+                highlight = state.activityDisplayRotation != lastState.activityDisplayRotation,
+            )
+            append(" / ")
+            boundsString(
+                state.applicationDisplayBounds,
+                highlight = state.applicationDisplayBounds != lastState.applicationDisplayBounds,
+            )
+            append(" / ")
+            boundsString(
+                state.activityDisplayBounds,
+                highlight = state.activityDisplayBounds != lastState.activityDisplayBounds,
+            )
+        },
+        style = MaterialTheme.typography.bodySmall,
+        overflow = TextOverflow.Ellipsis,
+        maxLines = 1,
+    )
+}
+
+/**
+ * Composes a detailed view for an expanded [WindowStateCard].
+ *
+ * @param state the [WindowState] to display.
+ * @param lastState the previous [WindowState] for comparison.
+ */
+@Composable
+private fun WindowStateDetail(state: WindowState, lastState: WindowState) {
+    val timestampTitle = stringResource(R.string.timestamp_title)
+    val applicationRotationTitle = stringResource(R.string.application_display_rotation_title)
+    val activityRotationTitle = stringResource(R.string.activity_display_rotation_title)
+    val applicationBoundsTittle = stringResource(R.string.application_display_bounds_title)
+    val activityBoundsTittle = stringResource(R.string.activity_display_bounds_title)
+
+    Text(
+        state.name,
+        style = MaterialTheme.typography.bodyLarge,
+        fontWeight = FontWeight.Bold,
+    )
+    Column {
+        Text(
+            "$timestampTitle ${state.timeStamp}",
+            style = MaterialTheme.typography.bodySmall,
+        )
+        DisplayRotationView(
+            applicationRotationTitle,
+            currentRotation = state.applicationDisplayRotation,
+            lastRotation = lastState.applicationDisplayRotation,
+        )
+        DisplayRotationView(
+            activityRotationTitle,
+            currentRotation = state.activityDisplayRotation,
+            lastRotation = lastState.activityDisplayRotation,
+        )
+        DisplayBoundsView(
+            applicationBoundsTittle,
+            currentBounds = state.applicationDisplayBounds,
+            lastBound = lastState.applicationDisplayBounds,
+        )
+        DisplayBoundsView(
+            activityBoundsTittle,
+            currentBounds = state.activityDisplayBounds,
+            lastBound = lastState.activityDisplayBounds,
+        )
+        Text(
+            "Callback details: ${state.callbackDetails}",
+            style = MaterialTheme.typography.bodySmall,
+        )
+    }
+}
+
+/**
+ * Composes a view for displaying rotation information in [WindowStateDetail].
+ *
+ * @param title the title for this rotation information.
+ * @param currentRotation the current rotation value to display.
+ * @param lastRotation the previous rotation value for comparison.
+ */
+@Composable
+private fun DisplayRotationView(title: String, currentRotation: Int, lastRotation: Int) {
+    val shouldHighlightChange = currentRotation != lastRotation
+    Row {
+        Text(title, style = MaterialTheme.typography.bodySmall)
+        Text(
+            buildAnnotatedString { rotationString(currentRotation, shouldHighlightChange) },
+            style = MaterialTheme.typography.bodySmall,
+        )
+        if (shouldHighlightChange) {
+            Text(
+                text = lastRotation.toRotationStr(),
+                style =
+                    MaterialTheme.typography.bodySmall.copy(
+                        textDecoration = TextDecoration.LineThrough,
+                    ),
+                modifier = Modifier.padding(start = 2.dp),
+            )
+        }
+    }
+}
+
+/** Formats a rotation value into an annotated string. */
+private fun AnnotatedString.Builder.rotationString(rotation: Int, highlight: Boolean) {
+    val color = if (highlight) Color.Red else Color.Unspecified
+    withStyle(style = SpanStyle(color = color)) { append(rotation.toRotationStr()) }
+}
+
+/**
+ * Composes a view for displaying bounds information in [WindowStateDetail].
+ *
+ * @param title the title for this bounds information.
+ * @param currentBounds the current bounds value to display.
+ * @param lastBound the previous bounds value for comparison.
+ */
+@Composable
+private fun DisplayBoundsView(title: String, currentBounds: Rect, lastBound: Rect) {
+    val shouldHighlightChange = currentBounds != lastBound
+    Row {
+        Text(title, style = MaterialTheme.typography.bodySmall)
+        Column {
+            Text(
+                buildAnnotatedString { boundsString(currentBounds, shouldHighlightChange) },
+                style = MaterialTheme.typography.bodySmall,
+            )
+            if (shouldHighlightChange) {
+                Text(
+                    text = "$lastBound",
+                    style =
+                        MaterialTheme.typography.bodySmall.copy(
+                            textDecoration = TextDecoration.LineThrough,
+                        ),
+                )
+            }
+        }
+    }
+}
+
+/** Formats a bounds value into an annotated string. */
+private fun AnnotatedString.Builder.boundsString(bound: Rect, highlight: Boolean) {
+    val color = if (highlight) Color.Red else Color.Unspecified
+    withStyle(style = SpanStyle(color = color)) { append("$bound") }
+}
+
+/** Converts an integer rotation value to a human-readable string representation. */
+private fun Int.toRotationStr(): String =
+    when (this) {
+        ROTATION_0 -> "0°"
+        ROTATION_90 -> "90°"
+        ROTATION_180 -> "180°"
+        ROTATION_270 -> "270°"
+        else -> "Unknown"
+    }
+
+/** Converts a [Date] to a simple time string for summary view. */
+private fun Date.toSimpleTimeStr(): String = SimpleDateFormat("(HH:mm:ss)").format(this)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt
deleted file mode 100644
index a128b484..0000000
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateView.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.demo.coresdk
-
-import android.content.Context
-import android.graphics.Rect
-import android.hardware.display.DisplayManager
-import android.util.AttributeSet
-import android.util.Log
-import android.view.Display.DEFAULT_DISPLAY
-import android.view.LayoutInflater
-import android.view.WindowManager
-import android.widget.LinearLayout
-import androidx.window.demo.R
-import androidx.window.demo.databinding.WindowStateViewBinding
-import androidx.window.layout.WindowMetricsCalculator
-import java.text.SimpleDateFormat
-import java.util.Date
-
-/** View to show the display configuration from the latest update. */
-class WindowStateView
-@JvmOverloads
-constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-    defStyleRes: Int = 0
-) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
-
-    private var title: String = "N/A"
-
-    /**
-     * [DisplayManager]s and [WindowManager]s from `Activity` and `Application` are updated from
-     * different resource configurations, so we show config from them separately.
-     */
-    private val applicationDisplayManager: DisplayManager
-    private val activityDisplayManager: DisplayManager
-    private val windowMetricsCalculator: WindowMetricsCalculator
-
-    private val timestampView: WindowStateConfigView
-    private val applicationDisplayRotationView: WindowStateConfigView
-    private val activityDisplayRotationView: WindowStateConfigView
-    private val applicationDisplayBoundsView: WindowStateConfigView
-    private val activityDisplayBoundsView: WindowStateConfigView
-    private val prevApplicationDisplayRotationView: WindowStateConfigView
-    private val prevActivityDisplayRotationView: WindowStateConfigView
-    private val prevApplicationDisplayBoundsView: WindowStateConfigView
-    private val prevActivityDisplayBoundsView: WindowStateConfigView
-
-    private val shouldHidePrevConfig: Boolean
-    private var lastApplicationDisplayRotation = -1
-    private var lastActivityDisplayRotation = -1
-    private val lastApplicationDisplayBounds = Rect()
-    private val lastActivityDisplayBounds = Rect()
-
-    init {
-        applicationDisplayManager =
-            context.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
-        activityDisplayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
-        windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
-
-        val viewBinding = WindowStateViewBinding.inflate(LayoutInflater.from(context), this, true)
-        timestampView = viewBinding.timestampView
-        applicationDisplayRotationView = viewBinding.applicationDisplayRotationView
-        activityDisplayRotationView = viewBinding.activityDisplayRotationView
-        applicationDisplayBoundsView = viewBinding.applicationDisplayBoundsView
-        activityDisplayBoundsView = viewBinding.activityDisplayBoundsView
-        prevApplicationDisplayRotationView = viewBinding.prevApplicationDisplayRotationView
-        prevActivityDisplayRotationView = viewBinding.prevActivityDisplayRotationView
-        prevApplicationDisplayBoundsView = viewBinding.prevApplicationDisplayBoundsView
-        prevActivityDisplayBoundsView = viewBinding.prevActivityDisplayBoundsView
-
-        context.theme.obtainStyledAttributes(attrs, R.styleable.WindowStateView, 0, 0).apply {
-            try {
-                getString(R.styleable.WindowStateView_title)?.let {
-                    viewBinding.callbackTitle.text = it
-                    title = it
-                }
-                shouldHidePrevConfig = getBoolean(R.styleable.WindowStateView_hidePrevConfig, false)
-                if (shouldHidePrevConfig) {
-                    timestampView.visibility = GONE
-                    applicationDisplayRotationView.shouldHighlightChange = false
-                    activityDisplayRotationView.shouldHighlightChange = false
-                    applicationDisplayBoundsView.shouldHighlightChange = false
-                    activityDisplayBoundsView.shouldHighlightChange = false
-                    prevApplicationDisplayRotationView.visibility = GONE
-                    prevActivityDisplayRotationView.visibility = GONE
-                    prevApplicationDisplayBoundsView.visibility = GONE
-                    prevActivityDisplayBoundsView.visibility = GONE
-                } else {
-                    applicationDisplayRotationView.shouldHighlightChange = true
-                    activityDisplayRotationView.shouldHighlightChange = true
-                    applicationDisplayBoundsView.shouldHighlightChange = true
-                    activityDisplayBoundsView.shouldHighlightChange = true
-                }
-            } finally {
-                recycle()
-            }
-        }
-    }
-
-    /** Called when the corresponding system callback is invoked. */
-    fun onWindowStateCallbackInvoked() {
-        val applicationDisplayRotation =
-            applicationDisplayManager.getDisplay(DEFAULT_DISPLAY).rotation
-        val activityDisplayRotation = activityDisplayManager.getDisplay(DEFAULT_DISPLAY).rotation
-        val applicationDisplayBounds =
-            windowMetricsCalculator.computeMaximumWindowMetrics(context.applicationContext).bounds
-        val activityDisplayBounds =
-            windowMetricsCalculator.computeMaximumWindowMetrics(context).bounds
-
-        if (
-            shouldHidePrevConfig &&
-                applicationDisplayRotation == lastApplicationDisplayRotation &&
-                activityDisplayRotation == lastActivityDisplayRotation &&
-                applicationDisplayBounds == lastApplicationDisplayBounds &&
-                activityDisplayBounds == lastActivityDisplayBounds
-        ) {
-            // Skip if the state is unchanged.
-            return
-        }
-
-        if (!shouldHidePrevConfig) {
-            // Debug log for the change title.
-            Log.d(WindowStateCallbackActivity.TAG, title)
-        }
-
-        timestampView.updateValue(TIME_FORMAT.format(Date()))
-        applicationDisplayRotationView.updateValue(applicationDisplayRotation.toString())
-        activityDisplayRotationView.updateValue(activityDisplayRotation.toString())
-        applicationDisplayBoundsView.updateValue(applicationDisplayBounds.toString())
-        activityDisplayBoundsView.updateValue(activityDisplayBounds.toString())
-
-        if (!shouldHidePrevConfig && lastApplicationDisplayRotation != -1) {
-            // Skip if there is no previous value.
-            prevApplicationDisplayRotationView.updateValue(
-                lastApplicationDisplayRotation.toString()
-            )
-            prevActivityDisplayRotationView.updateValue(lastActivityDisplayRotation.toString())
-            prevApplicationDisplayBoundsView.updateValue(lastApplicationDisplayBounds.toString())
-            prevActivityDisplayBoundsView.updateValue(lastActivityDisplayBounds.toString())
-        }
-
-        lastApplicationDisplayRotation = applicationDisplayRotation
-        lastActivityDisplayRotation = activityDisplayRotation
-        lastApplicationDisplayBounds.set(applicationDisplayBounds)
-        lastActivityDisplayBounds.set(activityDisplayBounds)
-    }
-
-    companion object {
-        private val TIME_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
-    }
-}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt
new file mode 100644
index 0000000..3ecab868
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/coresdk/WindowStateViewModel.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.coresdk
+
+import android.graphics.Rect
+import android.view.Surface.ROTATION_0
+import androidx.compose.runtime.Immutable
+import androidx.lifecycle.ViewModel
+import java.util.Date
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** Represents the display configuration, including rotations, bounds, and callback source. */
+@Immutable
+data class WindowState(
+    val name: String,
+    val timeStamp: Date = Date(),
+    val applicationDisplayRotation: Int = ROTATION_0,
+    val activityDisplayRotation: Int = ROTATION_0,
+    val applicationDisplayBounds: Rect = Rect(),
+    val activityDisplayBounds: Rect = Rect(),
+    val callbackDetails: String = "",
+    val isDetailsExpanded: Boolean = false,
+) {
+    /**
+     * Compares the core state properties of this WindowState with another object.
+     *
+     * @param other the object to compare with.
+     * @return `true` if the core state properties are the same, `false` otherwise.
+     */
+    fun isSameState(other: Any?): Boolean =
+        (other is WindowState) &&
+            applicationDisplayRotation == other.applicationDisplayRotation &&
+            activityDisplayRotation == other.activityDisplayRotation &&
+            applicationDisplayBounds == other.applicationDisplayBounds &&
+            activityDisplayBounds == other.activityDisplayBounds
+}
+
+/**
+ * Manages the state of window events.
+ *
+ * This ViewModel maintains a list of [WindowState] objects, representing different window
+ * configurations over time. It provides methods to add new states, update existing ones, clear the
+ * list, and handle user interactions with the UI.
+ *
+ * @param initStates an optional initial list of [WindowState] objects to populate the ViewModel
+ *   with upon creation. Defaults to an empty list.
+ * @property windowStates the current list of window states, this is exposed for the UI to observe.
+ */
+class WindowStateViewModel(initStates: List<WindowState> = emptyList()) : ViewModel() {
+    private val _states = MutableStateFlow(initStates)
+    val windowStates: StateFlow<List<WindowState>> = _states.asStateFlow()
+
+    /** Clears all window states from the list. */
+    fun clearWindowStates() = _states.update { emptyList() }
+
+    /** Adds a new window state to the beginning of the list. */
+    fun onWindowStateCallback(state: WindowState) =
+        _states.update { listOf(state) + it } // Prepend to the list.
+
+    /** Updates the latest window state if it's different from the current latest state. */
+    fun updateLatestWindowState(state: WindowState) =
+        _states.update { if (state.isSameState(it.firstOrNull())) it else listOf(state) + it }
+
+    /** Toggles the expanded state of a window state at the given index. */
+    fun onWindowStateItemClick(index: Int) =
+        _states.update { states -> states.updateAtIndex(index) { it.toggleExpand() } }
+}
+
+/**
+ * Toggles the expanded state of a WindowState.
+ *
+ * @return a new [WindowState] with the `isDetailsExpanded` property toggled.
+ */
+private fun WindowState.toggleExpand(): WindowState = copy(isDetailsExpanded = !isDetailsExpanded)
+
+/**
+ * Updates an item at a specific index in a list using the provided transform function.
+ *
+ * @param index the index of the item to update.
+ * @param transform the function to apply to the item at the specified index.
+ * @return a new list with the item at the specified index updated.
+ */
+private fun <T> List<T>.updateAtIndex(index: Int, transform: (T) -> T): List<T> =
+    mapIndexed { idx, value ->
+        if (idx == index) transform(value) else value
+    }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
index c915ee8..83ac0b2 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
@@ -17,7 +17,6 @@
 package androidx.window.demo.demos
 
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.RecyclerView
 import androidx.window.demo.DisplayFeaturesLetterboxLandscapeSlimActivity
 import androidx.window.demo.DisplayFeaturesLetterboxPortraitSlimActivity
@@ -35,10 +34,12 @@
 import androidx.window.demo.R.string.show_all_display_features_portrait_slim
 import androidx.window.demo.SplitLayoutActivity
 import androidx.window.demo.WindowMetricsActivity
+import androidx.window.demo.area.RearDisplayPresentationActivity
 import androidx.window.demo.common.DisplayFeaturesActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
 
 /** Main activity that launches WindowManager demos. */
-class WindowDemosActivity : AppCompatActivity() {
+class WindowDemosActivity : EdgeToEdgeActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -84,6 +85,11 @@
                     buttonTitle = getString(R.string.ime),
                     description = getString(R.string.ime_demo_description),
                     clazz = ImeActivity::class.java
+                ),
+                DemoItem(
+                    buttonTitle = getString(R.string.dual_display),
+                    description = getString(R.string.dual_display_description),
+                    clazz = RearDisplayPresentationActivity::class.java
                 )
             )
         val recyclerView = findViewById<RecyclerView>(R.id.demo_recycler_view)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
index 7f1e0a7..b5b677a 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
@@ -16,9 +16,15 @@
 
 package androidx.window.demo.embedding
 
+import android.graphics.Color
 import androidx.annotation.GuardedBy
+import androidx.window.demo.embedding.OverlayActivityBase.Companion.DEFAULT_OVERLAY_ATTRIBUTES
+import androidx.window.embedding.EmbeddingAnimationBackground
+import androidx.window.embedding.EmbeddingAnimationParams
+import androidx.window.embedding.OverlayAttributes
 import androidx.window.embedding.SplitAttributes
 import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
 
@@ -55,6 +61,71 @@
 
     @GuardedBy("lock") private var splitTypeLocked = SplitAttributes.SplitType.SPLIT_TYPE_EQUAL
 
+    @GuardedBy("lock") private var animationBackgroundLocked = EmbeddingAnimationBackground.DEFAULT
+
+    @GuardedBy("lock")
+    private var openAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+    @GuardedBy("lock")
+    private var closeAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+    @GuardedBy("lock")
+    private var changeAnimationLocked = EmbeddingAnimationParams.AnimationSpec.DEFAULT
+
+    internal var animationBackground: EmbeddingAnimationBackground
+        get() {
+            lock.withLock {
+                return animationBackgroundLocked
+            }
+        }
+        set(value) {
+            lock.withLock { animationBackgroundLocked = value }
+        }
+
+    internal var openAnimation: EmbeddingAnimationParams.AnimationSpec
+        get() {
+            lock.withLock {
+                return openAnimationLocked
+            }
+        }
+        set(value) {
+            lock.withLock { openAnimationLocked = value }
+        }
+
+    internal var closeAnimation: EmbeddingAnimationParams.AnimationSpec
+        get() {
+            lock.withLock {
+                return closeAnimationLocked
+            }
+        }
+        set(value) {
+            lock.withLock { closeAnimationLocked = value }
+        }
+
+    internal var changeAnimation: EmbeddingAnimationParams.AnimationSpec
+        get() {
+            lock.withLock {
+                return changeAnimationLocked
+            }
+        }
+        set(value) {
+            lock.withLock { changeAnimationLocked = value }
+        }
+
+    internal var overlayAttributes: OverlayAttributes
+        get() {
+            lock.withLock {
+                return overlayAttributesLocked
+            }
+        }
+        set(value) {
+            lock.withLock { overlayAttributesLocked = value }
+        }
+
+    @GuardedBy("lock") private var overlayAttributesLocked = DEFAULT_OVERLAY_ATTRIBUTES
+
+    internal var overlayMode = AtomicInteger()
+
     companion object {
         @Volatile private var globalInstance: DemoActivityEmbeddingController? = null
         private val globalLock = ReentrantLock()
@@ -71,5 +142,23 @@
             }
             return globalInstance!!
         }
+
+        /** Animation background constants. */
+        val ANIMATION_BACKGROUND_TEXTS = arrayOf("DEFAULT", "BLUE", "GREEN", "YELLOW")
+        val ANIMATION_BACKGROUND_VALUES =
+            arrayOf(
+                EmbeddingAnimationBackground.DEFAULT,
+                EmbeddingAnimationBackground.createColorBackground(Color.BLUE),
+                EmbeddingAnimationBackground.createColorBackground(Color.GREEN),
+                EmbeddingAnimationBackground.createColorBackground(Color.YELLOW)
+            )
+
+        /** Animation spec constants. */
+        val ANIMATION_SPEC_TEXTS = arrayOf("DEFAULT", "JUMP_CUT")
+        val ANIMATION_SPEC_VALUES =
+            arrayOf(
+                EmbeddingAnimationParams.AnimationSpec.DEFAULT,
+                EmbeddingAnimationParams.AnimationSpec.JUMP_CUT
+            )
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
new file mode 100644
index 0000000..ef31132
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.embedding
+
+import androidx.window.demo.common.EdgeToEdgeActivity
+
+/** Dialog style Activity. */
+open class DialogActivity : EdgeToEdgeActivity()
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
index 0c2288e..235558f 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -20,6 +20,10 @@
 import androidx.startup.Initializer
 import androidx.window.WindowSdkExtensions
 import androidx.window.demo.R
+import androidx.window.demo.embedding.OverlayActivityBase.Companion.OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
 import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_FULLSCREEN_TOGGLE
 import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_PLACEHOLDER
 import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.TAG_CUSTOMIZED_SPLIT_ATTRIBUTES
@@ -31,6 +35,15 @@
 import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP
 import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING
 import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_USE_DEFAULT_SPLIT_ATTRIBUTES
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.EmbeddingAnimationParams
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingConfiguration
+import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_TASK
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayAttributesCalculatorParams
+import androidx.window.embedding.OverlayController
 import androidx.window.embedding.RuleController
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
@@ -50,16 +63,31 @@
 /** Initializes SplitController with a set of statically defined rules. */
 class ExampleWindowInitializer : Initializer<RuleController> {
 
-    private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+    private lateinit var splitController: SplitController
+
+    private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
 
     override fun create(context: Context): RuleController {
-        SplitController.getInstance(context).apply {
-            if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
-                setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
+        splitController = SplitController.getInstance(context)
+
+        if (extensionVersion >= 2) {
+            splitController.setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
+        }
+        if (extensionVersion >= OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION) {
+            OverlayController.getInstance(context)
+                .setOverlayAttributesCalculator(::sampleOverlayAttributesCalculator)
+        }
+        ActivityEmbeddingController.getInstance(context).apply {
+            if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
+                setEmbeddingConfiguration(
+                    EmbeddingConfiguration.Builder().setDimAreaBehavior(ON_TASK).build()
+                )
             }
         }
         return RuleController.getInstance(context).apply {
-            if (SplitController.getInstance(context).splitSupportStatus == SPLIT_AVAILABLE) {
+            if (splitController.splitSupportStatus == SPLIT_AVAILABLE) {
                 setRules(RuleController.parseRules(context, R.xml.main_split_config))
             }
         }
@@ -79,7 +107,7 @@
             SplitAttributes.Builder().setSplitType(SPLIT_TYPE_EXPAND).build()
         if (
             tag?.startsWith(PREFIX_FULLSCREEN_TOGGLE) == true &&
-                mDemoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
+                demoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
         ) {
             return expandContainersAttrs
         }
@@ -89,12 +117,22 @@
         val isBookMode = windowLayoutInfo.isBookMode()
         val config = params.parentConfiguration
         val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
-        // Make a copy of the default splitAttributes, but replace the animation background
-        // color to what is configured in the Demo app.
+        // Always use the Demo app specified animation background.
+        val animationParams =
+            EmbeddingAnimationParams.Builder()
+                .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+                .setDemoAppAnimations(params.defaultSplitAttributes.animationParams)
+                .build()
         val defaultSplitAttributes =
             SplitAttributes.Builder()
                 .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
                 .setSplitType(params.defaultSplitAttributes.splitType)
+                .setAnimationParams(animationParams)
+                .apply {
+                    if (extensionVersion >= 6) {
+                        setDividerAttributes(params.defaultSplitAttributes.dividerAttributes)
+                    }
+                }
                 .build()
         when (
             tag?.removePrefix(PREFIX_FULLSCREEN_TOGGLE)
@@ -125,6 +163,7 @@
                                 TOP_TO_BOTTOM
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 } else if (isPortrait) {
                     return expandContainersAttrs
@@ -141,6 +180,7 @@
                                 TOP_TO_BOTTOM
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 }
             }
@@ -155,6 +195,7 @@
                                 TOP_TO_BOTTOM
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 } else {
                     SplitAttributes.Builder()
@@ -166,6 +207,7 @@
                                 LEFT_TO_RIGHT
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 }
             }
@@ -182,6 +224,7 @@
                                 TOP_TO_BOTTOM
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 } else {
                     SplitAttributes.Builder()
@@ -193,6 +236,7 @@
                                 LEFT_TO_RIGHT
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 }
             }
@@ -214,19 +258,72 @@
                                 if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
                             }
                         )
+                        .setAnimationParams(animationParams)
                         .build()
                 }
             }
             TAG_CUSTOMIZED_SPLIT_ATTRIBUTES -> {
                 return SplitAttributes.Builder()
-                    .setSplitType(mDemoActivityEmbeddingController.customizedSplitType)
-                    .setLayoutDirection(mDemoActivityEmbeddingController.customizedLayoutDirection)
+                    .setSplitType(demoActivityEmbeddingController.customizedSplitType)
+                    .setLayoutDirection(demoActivityEmbeddingController.customizedLayoutDirection)
+                    .setAnimationParams(animationParams)
                     .build()
             }
         }
         return defaultSplitAttributes
     }
 
+    private fun EmbeddingAnimationParams.Builder.setDemoAppAnimations(
+        params: EmbeddingAnimationParams
+    ): EmbeddingAnimationParams.Builder {
+        // Replace the transition animations with what is configured in the Demo app if the default
+        // splitAttributes' transition animations are applicable (all configured to default).
+        val useDemoAppAnimations =
+            params.openAnimation == AnimationSpec.DEFAULT &&
+                params.closeAnimation == AnimationSpec.DEFAULT &&
+                params.changeAnimation == AnimationSpec.DEFAULT
+        if (useDemoAppAnimations) {
+            setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+            setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+            setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+        } else {
+            setOpenAnimation(params.openAnimation)
+            setCloseAnimation(params.closeAnimation)
+            setChangeAnimation(params.changeAnimation)
+        }
+        return this
+    }
+
+    private fun sampleOverlayAttributesCalculator(
+        params: OverlayAttributesCalculatorParams
+    ): OverlayAttributes =
+        when (val mode = demoActivityEmbeddingController.overlayMode.get()) {
+            // Put the overlay to the right
+            OVERLAY_MODE_SIMPLE.value -> params.defaultOverlayAttributes
+            // Update the overlay with orientation:
+            // - Put the overlay to the bottom if the device is in portrait
+            // - Otherwise, put the overlay to the right if the device is in landscape
+            OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value ->
+                OverlayAttributes(
+                    if (params.parentWindowMetrics.isPortrait()) {
+                        EmbeddingBounds(
+                            EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+                            width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+                            height = EmbeddingBounds.Dimension.ratio(0.4f)
+                        )
+                    } else {
+                        EmbeddingBounds(
+                            EmbeddingBounds.Alignment.ALIGN_RIGHT,
+                            width = EmbeddingBounds.Dimension.ratio(0.5f),
+                            height = EmbeddingBounds.Dimension.ratio(0.8f)
+                        )
+                    }
+                )
+            // Fully customized overlay presentation
+            OVERLAY_MODE_CUSTOMIZATION.value -> demoActivityEmbeddingController.overlayAttributes
+            else -> throw IllegalStateException("Unknown mode $mode")
+        }
+
     private fun WindowMetrics.isPortrait(): Boolean = bounds.height() > bounds.width()
 
     private fun WindowLayoutInfo.isTabletop(): Boolean {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
index 886afee..55405f6 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
@@ -18,10 +18,10 @@
 
 import android.os.Bundle
 import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
 
 /** Activity to show a dialog. */
-class ExpandedDialogActivity : AppCompatActivity() {
+class ExpandedDialogActivity : EdgeToEdgeActivity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
new file mode 100644
index 0000000..2010317
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.embedding
+
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.RadioGroup
+import android.widget.SeekBar
+import android.widget.Spinner
+import android.widget.Toast
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivityOverlayActivityLayoutBinding
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
+import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
+import androidx.window.embedding.EmbeddingBounds.Dimension
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
+import androidx.window.embedding.SplitController
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
+import androidx.window.embedding.setLaunchingActivityStack
+import androidx.window.embedding.setOverlayCreateParams
+import kotlinx.coroutines.launch
+
+open class OverlayActivityBase :
+    EdgeToEdgeActivity(),
+    View.OnClickListener,
+    RadioGroup.OnCheckedChangeListener,
+    AdapterView.OnItemSelectedListener,
+    SeekBar.OnSeekBarChangeListener {
+
+    private val overlayTag = OverlayCreateParams.generateOverlayTag()
+
+    private lateinit var splitController: SplitController
+
+    private lateinit var overlayController: OverlayController
+
+    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+    private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
+
+    lateinit var viewBinding: ActivityOverlayActivityLayoutBinding
+
+    private var overlayActivityStack: ActivityStack? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        viewBinding = ActivityOverlayActivityLayoutBinding.inflate(layoutInflater)
+        splitController = SplitController.getInstance(this)
+        overlayController = OverlayController.getInstance(this)
+
+        if (
+            splitController.splitSupportStatus != SPLIT_AVAILABLE ||
+                extensionVersion < OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION
+        ) {
+            Toast.makeText(this, R.string.toast_show_overlay_warning, Toast.LENGTH_SHORT).show()
+            finish()
+            return
+        }
+
+        viewBinding.root.setBackgroundColor(Color.parseColor("#fff3e0"))
+        setContentView(viewBinding.root)
+
+        viewBinding.buttonUpdateOverlayLayout.setOnClickListener(this)
+        viewBinding.buttonLaunchOverlayContainer.setOnClickListener(this)
+        viewBinding.buttonLaunchOverlayActivityA.setOnClickListener(this)
+        viewBinding.buttonLaunchOverlayActivityB.setOnClickListener(this)
+        viewBinding.buttonFinishThisActivity.setOnClickListener(this)
+
+        val radioGroupChooseOverlayLayout = viewBinding.radioGroupChooseOverlayLayout
+        radioGroupChooseOverlayLayout.setOnCheckedChangeListener(this)
+
+        viewBinding.spinnerAlignment.apply {
+            adapter =
+                ArrayAdapter(
+                    this@OverlayActivityBase,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    POSITION_TEXT_ARRAY,
+                )
+            onItemSelectedListener = this@OverlayActivityBase
+        }
+
+        val dimensionAdapter =
+            ArrayAdapter(
+                this,
+                android.R.layout.simple_spinner_dropdown_item,
+                DIMENSION_TYPE_TEXT_ARRAY,
+            )
+        viewBinding.spinnerWidth.apply {
+            adapter = dimensionAdapter
+            onItemSelectedListener = this@OverlayActivityBase
+        }
+
+        viewBinding.spinnerHeight.apply {
+            adapter = dimensionAdapter
+            onItemSelectedListener = this@OverlayActivityBase
+        }
+
+        viewBinding.seekBarHeightInRatio.setOnSeekBarChangeListener(this)
+        viewBinding.seekBarWidthInRatio.setOnSeekBarChangeListener(this)
+
+        initializeUi()
+
+        lifecycleScope.launch {
+            // The block passed to repeatOnLifecycle is executed when the lifecycle
+            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+            // It automatically restarts the block when the lifecycle is STARTED again.
+            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                overlayController.overlayInfo(overlayTag).collect { overlayInfo ->
+                    overlayActivityStack = overlayInfo.activityStack
+                    val hasOverlay = overlayActivityStack != null
+                    viewBinding.buttonUpdateOverlayLayout.isEnabled =
+                        hasOverlay &&
+                            demoActivityEmbeddingController.overlayMode.get() !=
+                                OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
+                    updateOverlayBoundsText(overlayInfo)
+                }
+            }
+        }
+    }
+
+    private fun initializeUi() {
+        viewBinding.buttonUpdateOverlayLayout.isEnabled = false
+        viewBinding.radioGroupChooseOverlayLayout.check(R.id.radioButton_simple_overlay)
+        viewBinding.spinnerAlignment.setSelection(ALIGNMENT_VALUE_ARRAY.indexOf(ALIGN_RIGHT))
+        viewBinding.spinnerWidth.apply {
+            setSelection(INDEX_DIMENSION_RATIO)
+            updateDimensionUi(this)
+        }
+        viewBinding.spinnerHeight.apply {
+            setSelection(INDEX_DIMENSION_RATIO)
+            updateDimensionUi(this)
+        }
+    }
+
+    private fun initializeSeekbar(seekBar: SeekBar) {
+        seekBar.progress = 50
+        updateRatioText(seekBar)
+    }
+
+    private fun updateOverlayBoundsText(overlayInfo: OverlayInfo) {
+        viewBinding.textViewOverlayBounds.text =
+            resources.getString(R.string.overlay_bounds_text) +
+                overlayInfo.currentOverlayAttributes?.bounds.toString()
+    }
+
+    override fun onClick(button: View) {
+        val overlayAttributes = buildOverlayAttributesFromUi()
+        val isCustomizationMode =
+            demoActivityEmbeddingController.overlayMode.get() == OVERLAY_MODE_CUSTOMIZATION.value
+        when (button.id) {
+            R.id.button_launch_overlay_container -> {
+                if (isCustomizationMode) {
+                    // Also update controller's overlayAttributes because the launch bounds are
+                    // determined by calculator, which returns the overlayAttributes from
+                    // the controller directly.
+                    demoActivityEmbeddingController.overlayAttributes = overlayAttributes
+                }
+                try {
+                    startActivity(
+                        Intent().apply {
+                            setClassName(
+                                "androidx.window.demo2",
+                                "androidx.window.demo2.embedding.UntrustedEmbeddingActivity"
+                            )
+                        },
+                        ActivityOptions.makeBasic()
+                            .toBundle()
+                            .setOverlayCreateParams(
+                                this,
+                                OverlayCreateParams.Builder()
+                                    .setTag(overlayTag)
+                                    .setOverlayAttributes(
+                                        if (isCustomizationMode) {
+                                            overlayAttributes
+                                        } else {
+                                            DEFAULT_OVERLAY_ATTRIBUTES
+                                        }
+                                    )
+                                    .build()
+                            )
+                    )
+                } catch (e: ActivityNotFoundException) {
+                    Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show()
+                }
+            }
+            R.id.button_launch_overlay_activity_a ->
+                startActivity(
+                    Intent(this, OverlayAssociatedActivityA::class.java).apply {
+                        if (viewBinding.checkboxReorderToFront.isChecked) {
+                            flags = FLAG_ACTIVITY_REORDER_TO_FRONT
+                        }
+                    }
+                )
+            R.id.button_launch_overlay_activity_b ->
+                startActivity(
+                    Intent(this, OverlayAssociatedActivityB::class.java),
+                    overlayActivityStack?.let {
+                        if (viewBinding.checkboxLaunchToOverlay.isChecked) {
+                            ActivityOptions.makeBasic()
+                                .toBundle()
+                                .setLaunchingActivityStack(
+                                    this,
+                                    it,
+                                )
+                        } else {
+                            null
+                        }
+                    }
+                )
+            R.id.button_finish_this_activity -> finish()
+            R.id.button_update_overlay_layout -> {
+                if (isCustomizationMode) {
+                    demoActivityEmbeddingController.overlayAttributes = overlayAttributes
+                    ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
+                } else {
+                    overlayController.updateOverlayAttributes(overlayTag, overlayAttributes)
+                }
+            }
+        }
+    }
+
+    private fun buildOverlayAttributesFromUi(): OverlayAttributes {
+        val spinnerPosition = viewBinding.spinnerAlignment
+        val spinnerWidth = viewBinding.spinnerWidth
+        val spinnerHeight = viewBinding.spinnerHeight
+
+        return OverlayAttributes.Builder()
+            .setBounds(
+                EmbeddingBounds(
+                    ALIGNMENT_VALUE_ARRAY[spinnerPosition.selectedItemPosition],
+                    createDimensionFromUi(spinnerWidth),
+                    createDimensionFromUi(spinnerHeight),
+                )
+            )
+            .build()
+    }
+
+    private fun createDimensionFromUi(spinner: Spinner): Dimension =
+        when (val position = spinner.selectedItemPosition) {
+            INDEX_DIMENSION_EXPAND -> Dimension.DIMENSION_EXPANDED
+            INDEX_DIMENSION_HINGE -> Dimension.DIMENSION_HINGE
+            INDEX_DIMENSION_RATIO ->
+                Dimension.ratio(
+                    if (spinner.isSpinnerWidth()) {
+                        viewBinding.seekBarWidthInRatio.progress.toFloat() / 100
+                    } else {
+                        viewBinding.seekBarHeightInRatio.progress.toFloat() / 100
+                    }
+                )
+            INDEX_DIMENSION_PIXEL ->
+                Dimension.pixel(
+                    if (spinner.isSpinnerWidth()) {
+                        viewBinding.editTextNumberDecimalWidthInPixel.text.toString().toInt()
+                    } else {
+                        viewBinding.editTextNumberDecimalHeightInPixel.text.toString().toInt()
+                    }
+                )
+            else -> throw IllegalStateException("Unknown spinner index: $position")
+        }
+
+    override fun onCheckedChanged(group: RadioGroup, id: Int) {
+        demoActivityEmbeddingController.overlayMode.set(
+            when (id) {
+                R.id.radioButton_simple_overlay -> OVERLAY_MODE_SIMPLE.value
+                R.id.radioButton_change_with_orientation ->
+                    OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
+                R.id.radioButton_customization -> OVERLAY_MODE_CUSTOMIZATION.value
+                else -> throw IllegalArgumentException("Unrecognized id $id")
+            }
+        )
+    }
+
+    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+        if (
+            parent is Spinner &&
+                parent in arrayOf(viewBinding.spinnerWidth, viewBinding.spinnerHeight)
+        ) {
+            updateDimensionUi(parent)
+        }
+    }
+
+    private fun updateDimensionUi(spinner: Spinner) {
+        val textViewRatio =
+            if (spinner.isSpinnerWidth()) {
+                viewBinding.textViewWidthInRatio
+            } else {
+                viewBinding.textViewHeightInRatio
+            }
+        val seekBarRatio =
+            if (spinner.isSpinnerWidth()) {
+                viewBinding.seekBarWidthInRatio
+            } else {
+                viewBinding.seekBarHeightInRatio
+            }
+        val textViewPixel =
+            if (spinner.isSpinnerWidth()) {
+                viewBinding.textViewWidthInPixel
+            } else {
+                viewBinding.textViewHeightInPixel
+            }
+        val editTextPixel =
+            if (spinner.isSpinnerWidth()) {
+                viewBinding.editTextNumberDecimalWidthInPixel
+            } else {
+                viewBinding.editTextNumberDecimalHeightInPixel
+            }
+        when (spinner.selectedItemPosition) {
+            INDEX_DIMENSION_EXPAND,
+            INDEX_DIMENSION_HINGE -> {
+                textViewRatio.visibility = View.GONE
+                seekBarRatio.visibility = View.GONE
+                textViewPixel.visibility = View.GONE
+                editTextPixel.visibility = View.GONE
+            }
+            INDEX_DIMENSION_RATIO -> {
+                textViewRatio.visibility = View.VISIBLE
+                seekBarRatio.visibility = View.VISIBLE
+                textViewPixel.visibility = View.GONE
+                editTextPixel.visibility = View.GONE
+                initializeSeekbar(seekBarRatio)
+            }
+            INDEX_DIMENSION_PIXEL -> {
+                textViewRatio.visibility = View.GONE
+                seekBarRatio.visibility = View.GONE
+                textViewPixel.visibility = View.VISIBLE
+                editTextPixel.visibility = View.VISIBLE
+                editTextPixel.text.clear()
+            }
+        }
+    }
+
+    private fun Spinner.isSpinnerWidth() = this == viewBinding.spinnerWidth
+
+    override fun onNothingSelected(view: AdapterView<*>?) {
+        // Auto-generated method stub
+    }
+
+    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+        updateRatioText(seekBar)
+    }
+
+    private fun updateRatioText(seekBar: SeekBar) {
+        if (seekBar.isSeekBarWidthInRatio()) {
+            viewBinding.textViewWidthInRatio.text =
+                resources.getString(R.string.width_in_ratio) +
+                    (seekBar.progress.toFloat() / 100).toString()
+        } else {
+            viewBinding.textViewHeightInRatio.text =
+                resources.getString(R.string.height_in_ratio) +
+                    (seekBar.progress.toFloat() / 100).toString()
+        }
+    }
+
+    private fun SeekBar.isSeekBarWidthInRatio(): Boolean = this == viewBinding.seekBarWidthInRatio
+
+    override fun onStartTrackingTouch(seekBar: SeekBar) {
+        // Auto-generated method stub
+    }
+
+    override fun onStopTrackingTouch(seekBar: SeekBar) {
+        // Auto-generated method stub
+    }
+
+    @JvmInline
+    internal value class OverlayMode(val value: Int) {
+        companion object {
+            val OVERLAY_MODE_SIMPLE = OverlayMode(0)
+            val OVERLAY_MODE_CHANGE_WITH_ORIENTATION = OverlayMode(1)
+            val OVERLAY_MODE_CUSTOMIZATION = OverlayMode(2)
+        }
+    }
+
+    companion object {
+        internal const val OVERLAY_FEATURE_MINIMUM_REQUIRED_VERSION = 8
+
+        internal val DEFAULT_OVERLAY_ATTRIBUTES =
+            OverlayAttributes(
+                EmbeddingBounds(
+                    ALIGN_RIGHT,
+                    Dimension.ratio(0.5f),
+                    Dimension.ratio(0.8f),
+                )
+            )
+
+        private val POSITION_TEXT_ARRAY = arrayOf("top", "left", "bottom", "right")
+        private val ALIGNMENT_VALUE_ARRAY =
+            arrayListOf(
+                ALIGN_TOP,
+                ALIGN_LEFT,
+                ALIGN_BOTTOM,
+                ALIGN_RIGHT,
+            )
+
+        private val DIMENSION_TYPE_TEXT_ARRAY =
+            arrayOf(
+                "expand to the task",
+                "follow the hinge",
+                "dimension in ratio",
+                "dimension in pixel",
+            )
+        private const val INDEX_DIMENSION_EXPAND = 0
+        private const val INDEX_DIMENSION_HINGE = 1
+        private const val INDEX_DIMENSION_RATIO = 2
+        private const val INDEX_DIMENSION_PIXEL = 3
+    }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
new file mode 100644
index 0000000..0e35bb5
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.embedding
+
+class OverlayAssociatedActivityA : OverlayActivityBase()
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
new file mode 100644
index 0000000..edc9b7e
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.demo.embedding
+
+import android.graphics.Color
+import android.os.Bundle
+
+class OverlayAssociatedActivityB : OverlayActivityBase() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        viewBinding.rootOverlayActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
+    }
+}
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
index aec91b9..4520b2c 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.View
 import androidx.window.demo.R
@@ -25,8 +26,9 @@
 open class SplitActivityB : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#fff3e0"))
+        val color = Color.parseColor("#fff3e0")
+        findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+        window.setBackgroundDrawable(ColorDrawable(color))
 
         if (intent.getBooleanExtra(EXTRA_LAUNCH_C_TO_SIDE, false)) {
             startActivity(Intent(this, SplitActivityC::class.java))
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
index 7ff6884..de4bcef 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
@@ -18,12 +18,15 @@
 
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 
-import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_AVAILABLE;
+import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED;
+import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_UNAVAILABLE;
 import static androidx.window.embedding.SplitRule.FinishBehavior.ADJACENT;
 import static androidx.window.embedding.SplitRule.FinishBehavior.ALWAYS;
 import static androidx.window.embedding.SplitRule.FinishBehavior.NEVER;
 
 import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -36,14 +39,21 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.util.Consumer;
 import androidx.window.WindowSdkExtensions;
 import androidx.window.demo.R;
+import androidx.window.demo.common.EdgeToEdgeActivity;
 import androidx.window.demo.databinding.ActivitySplitActivityLayoutBinding;
 import androidx.window.embedding.ActivityEmbeddingController;
+import androidx.window.embedding.ActivityEmbeddingOptions;
 import androidx.window.embedding.ActivityFilter;
 import androidx.window.embedding.ActivityRule;
+import androidx.window.embedding.DividerAttributes;
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes;
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes;
+import androidx.window.embedding.EmbeddedActivityWindowInfo;
+import androidx.window.embedding.EmbeddingAnimationParams;
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec;
 import androidx.window.embedding.EmbeddingRule;
 import androidx.window.embedding.RuleController;
 import androidx.window.embedding.SplitAttributes;
@@ -51,7 +61,9 @@
 import androidx.window.embedding.SplitInfo;
 import androidx.window.embedding.SplitPairFilter;
 import androidx.window.embedding.SplitPairRule;
+import androidx.window.embedding.SplitPinRule;
 import androidx.window.embedding.SplitPlaceholderRule;
+import androidx.window.java.embedding.ActivityEmbeddingControllerCallbackAdapter;
 import androidx.window.java.embedding.SplitControllerCallbackAdapter;
 
 import java.util.HashSet;
@@ -62,7 +74,7 @@
  * Sample showcase of split activity rules. Allows the user to select some split configuration
  * options with checkboxes and launch activities with those options applied.
  */
-public class SplitActivityBase extends AppCompatActivity
+public class SplitActivityBase extends EdgeToEdgeActivity
         implements CompoundButton.OnCheckedChangeListener {
 
     private static final String TAG = "SplitActivityTest";
@@ -76,7 +88,11 @@
      */
     private SplitControllerCallbackAdapter mSplitControllerAdapter;
     private RuleController mRuleController;
-    private SplitInfoCallback mCallback;
+    private SplitInfoCallback mSplitInfoCallback;
+
+    private ActivityEmbeddingController mActivityEmbeddingController;
+    private ActivityEmbeddingControllerCallbackAdapter mActivityEmbeddingControllerCallbackAdapter;
+    private EmbeddedActivityWindowInfoCallback mEmbeddedActivityWindowInfoCallbackCallback;
 
     private ActivitySplitActivityLayoutBinding mViewBinding;
 
@@ -86,9 +102,28 @@
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        final SplitController splitController = SplitController.getInstance(this);
+        final SplitController.SplitSupportStatus splitSupportStatus =
+                splitController.getSplitSupportStatus();
+        if (splitSupportStatus == SPLIT_UNAVAILABLE) {
+            Toast.makeText(this, R.string.toast_split_not_available,
+                    Toast.LENGTH_SHORT).show();
+            finish();
+            return;
+        } else if (splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
+            Toast.makeText(this, R.string.toast_split_not_support,
+                    Toast.LENGTH_SHORT).show();
+            finish();
+            return;
+        }
+
         mViewBinding = ActivitySplitActivityLayoutBinding.inflate(getLayoutInflater());
         setContentView(mViewBinding.getRoot());
 
+        final int extensionVersion = WindowSdkExtensions.getInstance().getExtensionVersion();
+        mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this);
+
         // Setup activity launch buttons and config options.
         mViewBinding.launchB.setOnClickListener((View v) ->
                 startActivity(new Intent(this, SplitActivityB.class)));
@@ -99,10 +134,19 @@
         });
         mViewBinding.launchE.setOnClickListener((View v) -> {
             Bundle bundle = null;
+            if (mViewBinding.setLaunchingEInActivityStack.isChecked()) {
+                try {
+                    bundle = ActivityEmbeddingOptions.setLaunchingActivityStack(
+                            ActivityOptions.makeBasic().toBundle(), this,
+                            mActivityEmbeddingController.getActivityStack(this));
+                } catch (UnsupportedOperationException ex) {
+                    Log.w(TAG, "#setLaunchingActivityStack is not supported", ex);
+                }
+            }
             startActivity(new Intent(this, SplitActivityE.class), bundle);
         });
-        if (WindowSdkExtensions.getInstance().getExtensionVersion() < 3) {
-            mViewBinding.setLaunchingEInActivityStack.setEnabled(false);
+        if (extensionVersion < 3) {
+            mViewBinding.setLaunchingEInActivityStack.setVisibility(View.GONE);
         }
         mViewBinding.launchF.setOnClickListener((View v) ->
                 startActivity(new Intent(this, SplitActivityF.class)));
@@ -158,6 +202,44 @@
         });
         mViewBinding.launchExpandedDialogButton.setOnClickListener((View v) ->
                 startActivity(new Intent(this, ExpandedDialogActivity.class)));
+        mViewBinding.launchDialogActivityButton.setOnClickListener((View v) ->
+                startActivity(new Intent(this, DialogActivity.class)));
+        mViewBinding.launchDialogButton.setOnClickListener((View v) ->
+                new AlertDialog.Builder(this)
+                        .setTitle("Alert dialog demo")
+                        .setMessage("This is a dialog demo").create().show());
+
+        if (extensionVersion < 5) {
+            mViewBinding.pinTopActivityStackButton.setVisibility(View.GONE);
+            mViewBinding.unpinTopActivityStackButton.setVisibility(View.GONE);
+        } else {
+            mViewBinding.pinTopActivityStackButton.setOnClickListener((View v) -> {
+                        splitController.pinTopActivityStack(getTaskId(),
+                                new SplitPinRule.Builder().setSticky(
+                                        mViewBinding.stickyPinRule.isChecked()).build());
+                    }
+            );
+            mViewBinding.unpinTopActivityStackButton.setOnClickListener((View v) -> {
+                        splitController.unpinTopActivityStack(getTaskId());
+                    }
+            );
+        }
+        if (extensionVersion < 6) {
+            mViewBinding.dividerCheckBox.setVisibility(View.GONE);
+            mViewBinding.draggableDividerCheckBox.setVisibility(View.GONE);
+        } else {
+            mViewBinding.dividerCheckBox.setOnCheckedChangeListener(this);
+            mViewBinding.draggableDividerCheckBox.setOnCheckedChangeListener(this);
+        }
+        if (extensionVersion < 7) {
+            mViewBinding.openAnimationJumpCutCheckBox.setVisibility(View.GONE);
+            mViewBinding.closeAnimationJumpCutCheckBox.setVisibility(View.GONE);
+            mViewBinding.changeAnimationJumpCutCheckBox.setVisibility(View.GONE);
+        } else {
+            mViewBinding.openAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+            mViewBinding.closeAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+            mViewBinding.changeAnimationJumpCutCheckBox.setOnCheckedChangeListener(this);
+        }
 
         // Listen for split configuration checkboxes to update the rules before launching
         // activities.
@@ -169,42 +251,84 @@
         mViewBinding.fullscreenECheckBox.setOnCheckedChangeListener(this);
         mViewBinding.splitWithFCheckBox.setOnCheckedChangeListener(this);
 
-        final SplitController splitController = SplitController.getInstance(this);
+        if (extensionVersion < 6) {
+            mViewBinding.buttonLaunchOverlayAssociatedActivity.setVisibility(View.GONE);
+        } else {
+            mViewBinding.buttonLaunchOverlayAssociatedActivity.setOnClickListener((View v) ->
+                    startActivity(new Intent(this, OverlayAssociatedActivityA.class)));
+        }
+
         mSplitControllerAdapter = new SplitControllerCallbackAdapter(splitController);
-        if (splitController.getSplitSupportStatus() != SPLIT_AVAILABLE) {
-            Toast.makeText(this, R.string.toast_split_not_support,
-                    Toast.LENGTH_SHORT).show();
-            finish();
-            return;
+        if (extensionVersion >= 6) {
+            mActivityEmbeddingControllerCallbackAdapter =
+                    new ActivityEmbeddingControllerCallbackAdapter(mActivityEmbeddingController);
+
+            // The EmbeddedActivityWindowInfoListener will only be triggered when the activity is
+            // embedded and visible (just like Activity#onConfigurationChanged).
+            // Register it in #onCreate instead of #onStart so that when the embedded status is
+            // changed to non-embedded before #onStart (like screen rotation when this activity is
+            // in background), the listener will be triggered right after #onStart.
+            // Otherwise, if registered in #onStart, it will not be triggered on registration
+            // because the activity is not embedded, which results it shows the stale info.
+            mEmbeddedActivityWindowInfoCallbackCallback = new EmbeddedActivityWindowInfoCallback();
+            mActivityEmbeddingControllerCallbackAdapter.addEmbeddedActivityWindowInfoListener(
+                    this, Runnable::run, mEmbeddedActivityWindowInfoCallbackCallback);
         }
         mRuleController = RuleController.getInstance(this);
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mActivityEmbeddingControllerCallbackAdapter != null) {
+            mActivityEmbeddingControllerCallbackAdapter.removeEmbeddedActivityWindowInfoListener(
+                    mEmbeddedActivityWindowInfoCallbackCallback);
+            mEmbeddedActivityWindowInfoCallbackCallback = null;
+        }
+    }
+
+    @Override
     protected void onStart() {
         super.onStart();
-        mCallback = new SplitInfoCallback();
-        mSplitControllerAdapter.addSplitListener(this, Runnable::run, mCallback);
+        mSplitInfoCallback = new SplitInfoCallback();
+        mSplitControllerAdapter.addSplitListener(this, Runnable::run, mSplitInfoCallback);
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        mSplitControllerAdapter.removeSplitListener(mCallback);
-        mCallback = null;
+        mSplitControllerAdapter.removeSplitListener(mSplitInfoCallback);
+        mSplitInfoCallback = null;
     }
 
     /** Updates the embedding status when receives callback from the extension. */
-    class SplitInfoCallback implements Consumer<List<SplitInfo>> {
+    private class SplitInfoCallback implements Consumer<List<SplitInfo>> {
         @Override
         public void accept(List<SplitInfo> splitInfoList) {
             runOnUiThread(() -> {
-                updateEmbeddedStatus();
+                if (mActivityEmbeddingControllerCallbackAdapter == null) {
+                    // Otherwise, the embedded status will be updated from
+                    // EmbeddedActivityWindowInfoCallback.
+                    updateEmbeddedStatus(mActivityEmbeddingController.isActivityEmbedded(
+                            SplitActivityBase.this));
+                }
                 updateCheckboxesFromCurrentConfig();
             });
         }
     }
 
+    /** Updates the embedding status when receives callback from the extension. */
+    private class EmbeddedActivityWindowInfoCallback implements
+            Consumer<EmbeddedActivityWindowInfo> {
+        @Override
+        public void accept(EmbeddedActivityWindowInfo embeddedActivityWindowInfo) {
+            runOnUiThread(() -> {
+                updateEmbeddedStatus(embeddedActivityWindowInfo.isEmbedded());
+                updateEmbeddedWindowInfo(embeddedActivityWindowInfo);
+            });
+        }
+    }
+
     /** Called on checkbox changed. */
     @Override
     public void onCheckedChanged(@NonNull CompoundButton c, boolean isChecked) {
@@ -328,8 +452,37 @@
     /** Updates the split rules based on the current selection on checkboxes. */
     private void updateRulesFromCheckboxes() {
         mRuleController.clearRules();
+
+        final DividerAttributes dividerAttributes;
+        if (mViewBinding.dividerCheckBox.isChecked()) {
+            if (mViewBinding.draggableDividerCheckBox.isChecked()) {
+                dividerAttributes = new DraggableDividerAttributes.Builder()
+                        .setWidthDp(1)
+                        .setDraggingToFullscreenAllowed(true)
+                        .build();
+            } else {
+                dividerAttributes = new FixedDividerAttributes.Builder().setWidthDp(1).build();
+            }
+        } else {
+            dividerAttributes = DividerAttributes.NO_DIVIDER;
+        }
+        final EmbeddingAnimationParams.Builder animationParamsBuilder =
+                new EmbeddingAnimationParams.Builder();
+        if (mViewBinding.openAnimationJumpCutCheckBox.isChecked()) {
+            animationParamsBuilder.setOpenAnimation(AnimationSpec.JUMP_CUT);
+        }
+        if (mViewBinding.closeAnimationJumpCutCheckBox.isChecked()) {
+            animationParamsBuilder.setCloseAnimation(AnimationSpec.JUMP_CUT);
+        }
+        if (mViewBinding.changeAnimationJumpCutCheckBox.isChecked()) {
+            animationParamsBuilder.setChangeAnimation(AnimationSpec.JUMP_CUT);
+        }
+        final EmbeddingAnimationParams animationParams = animationParamsBuilder.build();
+
         final SplitAttributes defaultSplitAttributes = new SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(SPLIT_RATIO))
+                .setDividerAttributes(dividerAttributes)
+                .setAnimationParams(animationParams)
                 .build();
 
         if (mViewBinding.splitMainCheckBox.isChecked()) {
@@ -349,6 +502,14 @@
             mRuleController.addRule(rule);
         }
 
+        mViewBinding.draggableDividerCheckBox.setEnabled(mViewBinding.dividerCheckBox.isChecked());
+        mViewBinding.openAnimationJumpCutCheckBox.setEnabled(
+                mViewBinding.splitMainCheckBox.isChecked());
+        mViewBinding.closeAnimationJumpCutCheckBox.setEnabled(
+                mViewBinding.splitMainCheckBox.isChecked());
+        mViewBinding.changeAnimationJumpCutCheckBox.setEnabled(
+                mViewBinding.splitMainCheckBox.isChecked());
+
         if (mViewBinding.usePlaceholderCheckBox.isChecked()) {
             // Split B with placeholder.
             final Set<ActivityFilter> activityFilters = new HashSet<>();
@@ -436,11 +597,21 @@
     }
 
     /** Updates the status label that says when an activity is embedded. */
-    void updateEmbeddedStatus() {
-        if (ActivityEmbeddingController.getInstance(this).isActivityEmbedded(this)) {
-            mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.VISIBLE);
-        } else {
-            mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.GONE);
+    private void updateEmbeddedStatus(boolean isEmbedded) {
+        mViewBinding.activityEmbeddedStatusTextView.setVisibility(isEmbedded
+                ? View.VISIBLE
+                : View.GONE);
+    }
+
+    private void updateEmbeddedWindowInfo(
+            @NonNull EmbeddedActivityWindowInfo info) {
+        Log.d(TAG, "EmbeddedActivityWindowInfo changed for r=" + this + "\ninfo=" + info);
+        if (!info.isEmbedded()) {
+            mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.GONE);
+            return;
         }
+        mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.VISIBLE);
+        mViewBinding.activityEmbeddedBoundsTextView.setText(
+                "Embedded bounds=" + info.getBoundsInParentHost());
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
index dcbbb3b..7b61240c 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
@@ -17,6 +17,7 @@
 package androidx.window.demo.embedding
 
 import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.View
 import androidx.window.demo.R
@@ -24,7 +25,8 @@
 open class SplitActivityC : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#e8f5e9"))
+        val color = Color.parseColor("#e8f5e9")
+        findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+        window.setBackgroundDrawable(ColorDrawable(color))
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
index 705dfee..000ff2b 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
@@ -17,6 +17,7 @@
 package androidx.window.demo.embedding
 
 import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.View
 import androidx.window.demo.R
@@ -24,7 +25,8 @@
 open class SplitActivityD : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#eeeeee"))
+        val color = Color.parseColor("#eeeeee")
+        findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+        window.setBackgroundDrawable(ColorDrawable(color))
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
index f4f6bff..7e79b83 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
@@ -19,27 +19,34 @@
 import android.content.Intent
 import android.graphics.Color
 import android.os.Bundle
-import android.view.View
 import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
-import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivitySplitActivityListDetailLayoutBinding
 
-open class SplitActivityDetail : AppCompatActivity() {
+open class SplitActivityDetail : EdgeToEdgeActivity() {
+
+    private lateinit var viewBinding: ActivitySplitActivityListDetailLayoutBinding
+    private lateinit var itemDetailTextView: TextView
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_split_activity_list_detail_layout)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#fff3e0"))
 
-        findViewById<TextView>(R.id.item_detail_text)
-            .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
+        viewBinding = ActivitySplitActivityListDetailLayoutBinding.inflate(layoutInflater)
+        viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#fff3e0"))
+        setContentView(viewBinding.root)
+        itemDetailTextView = viewBinding.itemDetailText
+
+        itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
+
+        window.decorView.setOnFocusChangeListener { _, focus ->
+            itemDetailTextView.text = "${itemDetailTextView.text} focus=$focus"
+        }
     }
 
     override fun onNewIntent(intent: Intent) {
         super.onNewIntent(intent)
 
-        findViewById<TextView>(R.id.item_detail_text)
-            .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
+        itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
     }
 
     companion object {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
index 2766a7c..a530d22 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
@@ -17,6 +17,7 @@
 package androidx.window.demo.embedding
 
 import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.View
 import androidx.window.demo.R
@@ -24,7 +25,8 @@
 open class SplitActivityE : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#ede7f6"))
+        val color = Color.parseColor("#ede7f6")
+        findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+        window.setBackgroundDrawable(ColorDrawable(color))
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
index 5fbf9ce..d8161d0 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
@@ -17,6 +17,7 @@
 package androidx.window.demo.embedding
 
 import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.view.View
 import androidx.window.demo.R
@@ -24,7 +25,8 @@
 open class SplitActivityF : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#ffebee"))
+        val color = Color.parseColor("#ffebee")
+        findViewById<View>(R.id.root_split_activity_layout).setBackgroundColor(color)
+        window.setBackgroundDrawable(ColorDrawable(color))
     }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
index b7920d8..d7e1cf4 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
@@ -21,23 +21,25 @@
 import android.os.Bundle
 import android.view.View
 import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
+import androidx.window.demo.databinding.ActivitySplitActivityListLayoutBinding
 import androidx.window.demo.embedding.SplitActivityDetail.Companion.EXTRA_SELECTED_ITEM
 import androidx.window.embedding.SplitController
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-open class SplitActivityList : AppCompatActivity() {
+private lateinit var viewBinding: ActivitySplitActivityListLayoutBinding
+
+open class SplitActivityList : EdgeToEdgeActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_split_activity_list_layout)
-        findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#e0f7fa"))
+        viewBinding = ActivitySplitActivityListLayoutBinding.inflate(layoutInflater)
+        setContentView(viewBinding.root)
+        viewBinding.root.setBackgroundColor(Color.parseColor("#e0f7fa"))
         val splitController = SplitController.getInstance(this)
 
         lifecycleScope.launch {
@@ -47,7 +49,7 @@
             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 splitController.splitInfoList(this@SplitActivityList).collect { newSplitInfos ->
                     withContext(Dispatchers.Main) {
-                        findViewById<View>(R.id.infoButton).visibility =
+                        viewBinding.infoButton.visibility =
                             if (newSplitInfos.isEmpty()) View.VISIBLE else View.GONE
                     }
                 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
index 687f931..921f72d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
@@ -18,10 +18,10 @@
 
 import android.graphics.Color
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.databinding.ActivitySplitActivityPlaceholderLayoutBinding
 
-open class SplitActivityPlaceholder : AppCompatActivity() {
+open class SplitActivityPlaceholder : EdgeToEdgeActivity() {
 
     lateinit var viewBinding: ActivitySplitActivityPlaceholderLayoutBinding
 
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
index 6badb3e..a2fa58be 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleActivityBase.kt
@@ -18,11 +18,11 @@
 
 import android.os.Bundle
 import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.embedding.RuleController
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
@@ -35,7 +35,7 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-open class SplitAttributesToggleActivityBase : AppCompatActivity() {
+open class SplitAttributesToggleActivityBase : EdgeToEdgeActivity() {
     internal lateinit var splitController: SplitController
     internal lateinit var ruleController: RuleController
 
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
index ae0246a..1ef955d 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
@@ -80,6 +80,7 @@
             splitRuleFoldingAwareAttrsRadioButton.isEnabled = false
             viewBinding.splitRuleUseCustomizedSplitAttributes.isEnabled = false
         }
+
         viewBinding.startPrimaryActivityButton.setOnClickListener(this)
         viewBinding.useStickyPlaceholderCheckBox.setOnCheckedChangeListener(this)
         viewBinding.usePlaceholderCheckBox.setOnCheckedChangeListener(this)
@@ -238,6 +239,13 @@
                 if (apiLevel < 3) {
                     append("Finishing secondary activities is not supported on this device!\n")
                 }
+                if (
+                    viewBinding.finishSecondaryActivitiesButton.isEnabled &&
+                        getSplitRule<SplitPlaceholderRule>() != null
+                ) {
+                    append(resources.getString(R.string.show_placeholder_warning))
+                    append("\n")
+                }
             }
         withContext(Dispatchers.Main) { viewBinding.warningMessageTextView.text = warningMessages }
     }
@@ -429,8 +437,20 @@
             R.id.split_rule_layout_direction_spinner ->
                 demoActivityEmbeddingController.customizedLayoutDirection =
                     CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
+            R.id.animation_background_dropdown ->
+                demoActivityEmbeddingController.animationBackground =
+                    DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
+            R.id.open_animation_dropdown ->
+                demoActivityEmbeddingController.openAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+            R.id.close_animation_dropdown ->
+                demoActivityEmbeddingController.closeAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+            R.id.change_animation_dropdown ->
+                demoActivityEmbeddingController.changeAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
         }
-        splitController.invalidateTopVisibleSplitAttributes()
+        activityEmbeddingController.invalidateVisibleActivityStacks()
     }
 
     override fun onNothingSelected(view: AdapterView<*>?) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
index 0e684d2..38587a7 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
@@ -21,14 +21,19 @@
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
+import android.widget.ArrayAdapter
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowSdkExtensions
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.R
 import androidx.window.embedding.ActivityStack
 import androidx.window.embedding.SplitInfo
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
+@OptIn(ExperimentalWindowApi::class)
 class SplitAttributesTogglePrimaryActivity :
     SplitAttributesToggleMainActivity(), View.OnClickListener {
 
@@ -40,6 +45,8 @@
 
         viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
 
+        val isRuntimeApiSupported = WindowSdkExtensions.getInstance().extensionVersion >= 3
+
         secondaryActivityIntent = Intent(this, SplitAttributesToggleSecondaryActivity::class.java)
 
         if (intent.getBooleanExtra(EXTRA_LAUNCH_SECONDARY, false)) {
@@ -51,6 +58,69 @@
 
         // Enable to finish secondary ActivityStacks for primary Activity.
         viewBinding.finishSecondaryActivitiesDivider.visibility = View.VISIBLE
+        val finishSecondaryActivitiesButton =
+            viewBinding.finishSecondaryActivitiesButton.apply {
+                visibility = View.VISIBLE
+                if (!isRuntimeApiSupported) {
+                    isEnabled = false
+                } else {
+                    setOnClickListener(this@SplitAttributesTogglePrimaryActivity)
+                }
+            }
+
+        // Animation background
+        if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
+            val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
+            animationBackgroundDropdown.visibility = View.VISIBLE
+            viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
+            viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
+            animationBackgroundDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
+                )
+            animationBackgroundDropdown.onItemSelectedListener = this
+        }
+
+        // Animation transitions
+        if (WindowSdkExtensions.getInstance().extensionVersion >= 7) {
+            val openAnimationDropdown = viewBinding.openAnimationDropdown
+            openAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.openAnimationDivider.visibility = View.VISIBLE
+            viewBinding.openAnimationTextView.visibility = View.VISIBLE
+            openAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            openAnimationDropdown.onItemSelectedListener = this
+
+            val closeAnimationDropdown = viewBinding.closeAnimationDropdown
+            closeAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.closeAnimationDivider.visibility = View.VISIBLE
+            viewBinding.closeAnimationTextView.visibility = View.VISIBLE
+            closeAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            closeAnimationDropdown.onItemSelectedListener = this
+
+            val changeAnimationDropdown = viewBinding.changeAnimationDropdown
+            changeAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.changeAnimationDivider.visibility = View.VISIBLE
+            viewBinding.changeAnimationTextView.visibility = View.VISIBLE
+            changeAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            changeAnimationDropdown.onItemSelectedListener = this
+        }
 
         lifecycleScope.launch {
             // The block passed to repeatOnLifecycle is executed when the lifecycle
@@ -61,6 +131,7 @@
                     .splitInfoList(this@SplitAttributesTogglePrimaryActivity)
                     .onEach { updateUiFromRules() }
                     .collect { splitInfoList ->
+                        finishSecondaryActivitiesButton.isEnabled = splitInfoList.isNotEmpty()
                         activityStacks =
                             splitInfoList.mapTo(mutableSetOf()) { splitInfo ->
                                 splitInfo.getTheOtherActivityStack(
@@ -78,4 +149,14 @@
         } else {
             primaryActivityStack
         }
+
+    override fun onClick(button: View) {
+        super.onClick(button)
+        when (button.id) {
+            R.id.finish_secondary_activities_button -> {
+                applyRules()
+                activityEmbeddingController.finishActivityStacks(activityStacks)
+            }
+        }
+    }
 }
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
index 129d0aa9..21cc4a7 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
@@ -175,7 +175,7 @@
                     val enableFullscreenMode =
                         DemoActivityEmbeddingController.getInstance().shouldExpandSecondaryContainer
                     enableFullscreenMode.set(!enableFullscreenMode.get())
-                    splitController.invalidateTopVisibleSplitAttributes()
+                    activityEmbeddingController.invalidateVisibleActivityStacks()
                 } else {
                     // Update the top splitInfo if single default split Attributes is used.
                     splitController.updateSplitAttributes(
@@ -201,7 +201,7 @@
                 demoActivityEmbeddingController.customizedLayoutDirection =
                     CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
         }
-        splitController.invalidateTopVisibleSplitAttributes()
+        activityEmbeddingController.invalidateVisibleActivityStacks()
     }
 
     override fun onNothingSelected(view: AdapterView<*>?) {
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
index a6c23d3..ff94d18 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
@@ -21,23 +21,26 @@
 import android.os.Bundle
 import android.view.View
 import android.widget.AdapterView
+import android.widget.ArrayAdapter
 import android.widget.CompoundButton
 import android.widget.RadioGroup
 import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.window.WindowSdkExtensions
 import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.databinding.ActivitySplitDeviceStateLayoutBinding
+import androidx.window.embedding.EmbeddingAnimationParams
 import androidx.window.embedding.EmbeddingRule
 import androidx.window.embedding.RuleController
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
 import androidx.window.embedding.SplitController
-import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_ERROR_PROPERTY_NOT_DECLARED
+import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
 import androidx.window.embedding.SplitInfo
 import androidx.window.embedding.SplitPairFilter
 import androidx.window.embedding.SplitPairRule
@@ -46,7 +49,7 @@
 import kotlinx.coroutines.withContext
 
 open class SplitDeviceStateActivityBase :
-    AppCompatActivity(),
+    EdgeToEdgeActivity(),
     View.OnClickListener,
     RadioGroup.OnCheckedChangeListener,
     CompoundButton.OnCheckedChangeListener,
@@ -64,6 +67,8 @@
     private lateinit var activityA: ComponentName
     private lateinit var activityB: ComponentName
 
+    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
     /** The last selected split rule id. */
     private var lastCheckedRuleId = 0
 
@@ -73,7 +78,11 @@
         super.onCreate(savedInstanceState)
         viewBinding = ActivitySplitDeviceStateLayoutBinding.inflate(layoutInflater)
         splitController = SplitController.getInstance(this)
-        if (splitController.splitSupportStatus != SPLIT_AVAILABLE) {
+        if (splitController.splitSupportStatus == SPLIT_UNAVAILABLE) {
+            Toast.makeText(this, R.string.toast_split_not_available, Toast.LENGTH_SHORT).show()
+            finish()
+            return
+        } else if (splitController.splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
             Toast.makeText(this, R.string.toast_split_not_support, Toast.LENGTH_SHORT).show()
             finish()
             return
@@ -118,6 +127,62 @@
                 resources.getString(R.string.split_attributes_calculator_not_supported)
         }
 
+        // Animation background
+        if (WindowSdkExtensions.getInstance().extensionVersion >= 5 && componentName == activityA) {
+            // Show on only the primary activity.
+            val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
+            animationBackgroundDropdown.visibility = View.VISIBLE
+            viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
+            viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
+            animationBackgroundDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
+                )
+            animationBackgroundDropdown.onItemSelectedListener = this
+        }
+
+        // Animation transitions
+        if (WindowSdkExtensions.getInstance().extensionVersion >= 7 && componentName == activityA) {
+            // Show on only the primary activity.
+            val openAnimationDropdown = viewBinding.openAnimationDropdown
+            openAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.openAnimationDivider.visibility = View.VISIBLE
+            viewBinding.openAnimationTextView.visibility = View.VISIBLE
+            openAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            openAnimationDropdown.onItemSelectedListener = this
+
+            val closeAnimationDropdown = viewBinding.closeAnimationDropdown
+            closeAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.closeAnimationDivider.visibility = View.VISIBLE
+            viewBinding.closeAnimationTextView.visibility = View.VISIBLE
+            closeAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            closeAnimationDropdown.onItemSelectedListener = this
+
+            val changeAnimationDropdown = viewBinding.changeAnimationDropdown
+            changeAnimationDropdown.visibility = View.VISIBLE
+            viewBinding.changeAnimationDivider.visibility = View.VISIBLE
+            viewBinding.changeAnimationTextView.visibility = View.VISIBLE
+            changeAnimationDropdown.adapter =
+                ArrayAdapter(
+                    this,
+                    android.R.layout.simple_spinner_dropdown_item,
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_TEXTS
+                )
+            changeAnimationDropdown.onItemSelectedListener = this
+        }
+
         lifecycleScope.launch {
             // The block passed to repeatOnLifecycle is executed when the lifecycle
             // is at least STARTED and is cancelled when the lifecycle is STOPPED.
@@ -171,6 +236,20 @@
     }
 
     override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+        when (parent?.id) {
+            R.id.animation_background_dropdown ->
+                demoActivityEmbeddingController.animationBackground =
+                    DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
+            R.id.open_animation_dropdown ->
+                demoActivityEmbeddingController.openAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+            R.id.close_animation_dropdown ->
+                demoActivityEmbeddingController.closeAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+            R.id.change_animation_dropdown ->
+                demoActivityEmbeddingController.changeAnimation =
+                    DemoActivityEmbeddingController.ANIMATION_SPEC_VALUES[position]
+        }
         updateSplitPairRuleWithRadioButtonId(lastCheckedRuleId)
     }
 
@@ -246,6 +325,14 @@
             SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_EQUAL)
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+                        .setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+                        .setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+                        .setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+                        .build()
+                )
                 .build()
         // Use the tag to control the rule how to change split attributes with the current state
         var tag =
@@ -292,7 +379,17 @@
 
     private suspend fun updateSplitAttributesText(newSplitInfos: List<SplitInfo>) {
         var splitAttributes: SplitAttributes =
-            SplitAttributes.Builder().setSplitType(SPLIT_TYPE_EXPAND).build()
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_EXPAND)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
+                        .setOpenAnimation(demoActivityEmbeddingController.openAnimation)
+                        .setCloseAnimation(demoActivityEmbeddingController.closeAnimation)
+                        .setChangeAnimation(demoActivityEmbeddingController.changeAnimation)
+                        .build()
+                )
+                .build()
         var suggestToFinishItself = false
 
         // Traverse SplitInfos from the end because last SplitInfo has the highest z-order.
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
index aa3bdbc..d167778 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitImeActivityBase.kt
@@ -17,14 +17,14 @@
 package androidx.window.demo.embedding
 
 import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.databinding.ActivitySplitImeLayoutBinding
 
 /**
  * Sample showcase of split activity with input field. Allows the user to use IME with split
  * activities. The split rule is defined in `main_split_config.xml`.
  */
-abstract class SplitImeActivityBase : AppCompatActivity() {
+abstract class SplitImeActivityBase : EdgeToEdgeActivity() {
 
     lateinit var viewBinding: ActivitySplitImeLayoutBinding
 
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
index ae74e8f..a626a67 100644
--- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
@@ -24,11 +24,11 @@
 import android.widget.CompoundButton
 import android.widget.RadioGroup
 import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.window.demo.R
+import androidx.window.demo.common.EdgeToEdgeActivity
 import androidx.window.demo.common.util.PictureInPictureUtil
 import androidx.window.demo.databinding.ActivitySplitPipActivityLayoutBinding
 import androidx.window.embedding.ActivityFilter
@@ -53,7 +53,7 @@
  * applied.
  */
 abstract class SplitPipActivityBase :
-    AppCompatActivity(),
+    EdgeToEdgeActivity(),
     CompoundButton.OnCheckedChangeListener,
     View.OnClickListener,
     RadioGroup.OnCheckedChangeListener {
diff --git a/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml
deleted file mode 100644
index 449fe9a..0000000
--- a/window/window-demos/demo/src/main/res/layout/activity_coresdk_window_state_callback_layout.xml
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  Copyright 2023 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<ScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/root_split_activity_layout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:padding="10dp">
-
-        <!-- Update to the latest configuration. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/latest_update_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/latest_configuration_title"
-            app:hidePrevConfig="true"/>
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="10dp"
-            android:layout_marginBottom="10dp"
-            android:background="#AAAAAA" />
-
-        <!-- Update from Application DisplayListener. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/application_display_listener_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/application_display_listener_title"/>
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="10dp"
-            android:layout_marginBottom="10dp"
-            android:background="#AAAAAA" />
-
-        <!-- Update from Activity DisplayListener. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/activity_display_listener_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/activity_display_listener_title"/>
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="10dp"
-            android:layout_marginBottom="10dp"
-            android:background="#AAAAAA" />
-
-        <!-- Update from Application#onConfigurationChanged. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/application_configuration_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/application_configuration_title"/>
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="10dp"
-            android:layout_marginBottom="10dp"
-            android:background="#AAAAAA" />
-
-        <!-- Update from Activity#onConfigurationChanged. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/activity_configuration_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/activity_configuration_title"/>
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:layout_marginTop="10dp"
-            android:layout_marginBottom="10dp"
-            android:background="#AAAAAA" />
-
-        <!-- Update from WindowInfoTracker. -->
-        <androidx.window.demo.coresdk.WindowStateView
-            android:id="@+id/display_feature_view"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:title="@string/display_feature_title"/>
-
-    </LinearLayout>
-</ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
new file mode 100644
index 0000000..8650bcb
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/root_overlay_activity_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/textView_overlay_bounds"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/overlay_bounds_text" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginBottom="10dp"
+            android:layout_marginTop="10dp"
+            android:background="#AAAAAA" />
+
+        <TextView
+            android:id="@+id/textView_choose_overlay_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Choose overlay layout:" />
+
+        <RadioGroup
+            android:id="@+id/radioGroup_choose_overlay_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <RadioButton
+                android:id="@+id/radioButton_simple_overlay"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Launch the overlay container to the right" />
+
+            <RadioButton
+                android:id="@+id/radioButton_change_with_orientation"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Change overlay layout with orientation" />
+
+            <RadioButton
+                android:id="@+id/radioButton_customization"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Use customized overlay layout" />
+        </RadioGroup>
+
+        <TextView
+            android:id="@+id/textView_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Layout"
+            android:visibility="visible" />
+
+        <!-- UI to control position -->
+
+        <TextView
+            android:id="@+id/textView_position"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Position"
+            android:visibility="visible" />
+        <Spinner
+            android:id="@+id/spinner_alignment"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            tools:visibility="visible" />
+
+        <!-- UI to control width -->
+
+        <TextView
+            android:id="@+id/textView_width"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Width"
+            android:visibility="visible" />
+        <Spinner
+            android:id="@+id/spinner_width"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            tools:visibility="visible" />
+        <TextView
+            android:id="@+id/textView_width_in_pixel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Width in pixel"
+            android:visibility="gone" />
+        <EditText
+            android:id="@+id/editTextNumberDecimal_width_in_pixel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="4"
+            android:inputType="numberDecimal"
+            android:visibility="gone" />
+        <TextView
+            android:id="@+id/textView_width_in_ratio"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/width_in_ratio" />
+        <SeekBar
+            android:id="@+id/seekBar_width_in_ratio"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:min="1"
+            android:max="99"
+            android:progress="50" />
+
+        <!-- UI to control height -->
+
+        <TextView
+            android:id="@+id/textView_height"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Height"
+            android:visibility="visible" />
+        <Spinner
+            android:id="@+id/spinner_height"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            tools:visibility="visible" />
+        <TextView
+            android:id="@+id/textView_height_in_pixel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Height in pixel"
+            android:visibility="gone" />
+        <EditText
+            android:id="@+id/editTextNumberDecimal_height_in_pixel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ems="4"
+            android:inputType="numberDecimal"
+            android:visibility="gone" />
+        <TextView
+            android:id="@+id/textView_height_in_ratio"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/height_in_ratio"
+            android:visibility="gone" />
+        <SeekBar
+            android:id="@+id/seekBar_height_in_ratio"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:min="1"
+            android:max="99"
+            android:progress="80"
+            android:visibility="gone" />
+
+        <!-- UI to update overlay layout  -->
+
+        <Button
+            android:id="@+id/button_update_overlay_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Update Overlay layout" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginBottom="10dp"
+            android:layout_marginTop="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/button_launch_overlay_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Launch the overlay container" />
+
+        <CheckBox
+            android:id="@+id/checkbox_reorder_to_front"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Reorder Activity A to the front" />
+
+        <Button
+            android:id="@+id/button_launch_overlay_activity_a"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Launch Overlay Associated Activity A" />
+
+        <CheckBox
+            android:id="@+id/checkbox_launch_to_overlay"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Launch Activity B to the overlay container" />
+
+        <Button
+            android:id="@+id/button_launch_overlay_activity_b"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Launch Overlay Associated Activity B" />
+
+        <Button
+            android:id="@+id/button_finish_this_activity"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Finish this activity" />
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
index 43bea60..9d0c9c3 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
@@ -37,6 +37,17 @@
         android:layout_marginBottom="32dp"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/rear_display_session_button" />
+
+    <Button
+        android:id="@+id/rear_display_session_button"
+        android:text="Get active session if available"
+        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-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
new file mode 100644
index 0000000..ff5129d
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT 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_presentation_button"
+        android:text="Enable Rear Display Presentation"
+        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-demos/demo/src/main/res/layout/activity_split_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
index ab54e1e..0533be2 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
@@ -30,7 +30,15 @@
             android:id="@+id/activity_embedded_status_text_view"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:text="Activity is embedded" />
+            android:text="Activity is embedded"
+            android:visibility="gone" />
+
+        <TextView
+            android:id="@+id/activity_embedded_bounds_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Embedded bounds not available"
+            android:visibility="gone" />
 
         <CheckBox
             android:id="@+id/splitMainCheckBox"
@@ -38,6 +46,40 @@
             android:layout_height="wrap_content"
             android:text="Split Main with other activities" />
 
+        <CheckBox
+            android:id="@+id/dividerCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Add a divider between containers" />
+
+        <CheckBox
+            android:id="@+id/draggableDividerCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Make the divider draggable"
+            android:enabled="false"/>
+
+        <CheckBox
+            android:id="@+id/openAnimationJumpCutCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Use jump cut for split open animation"
+            android:enabled="false"/>
+
+        <CheckBox
+            android:id="@+id/closeAnimationJumpCutCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Use jump cut for split close animation"
+            android:enabled="false"/>
+
+        <CheckBox
+            android:id="@+id/changeAnimationJumpCutCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Use jump cut for split change animation"
+            android:enabled="false"/>
+
         <View
             android:layout_width="match_parent"
             android:layout_height="1dp"
@@ -193,5 +235,60 @@
             android:layout_height="wrap_content"
             android:layout_centerHorizontal="true"
             android:text="Launch Expanded Dialog" />
+
+        <Button
+            android:id="@+id/launch_dialog_activity_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch Dialog Activity" />
+
+        <Button
+            android:id="@+id/launch_dialog_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch Dialog" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/pin_top_activity_stack_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Pin Top ActivityStack" />
+
+        <CheckBox
+            android:id="@+id/stickyPinRule"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Set Pin Rule Sticky" />
+
+        <Button
+            android:id="@+id/unpin_top_activity_stack_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Unpin Top ActivityStack" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/button_launch_overlay_associated_activity"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch Overlay Associated Activity A" />
     </LinearLayout>
 </ScrollView>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
index dad1100..6acd57f 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
@@ -192,6 +192,106 @@
                     android:visibility="gone"/>
         </RadioGroup>
 
+        <!-- Dropdown for animation background -->
+
+        <View
+            android:id="@+id/animation_background_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/animation_background_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_animation_background"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/animation_background_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for open animation transition -->
+
+        <View
+            android:id="@+id/open_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/open_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_open_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/open_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for close animation transition -->
+
+        <View
+            android:id="@+id/close_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/close_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_close_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/close_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for change animation transition -->
+
+        <View
+            android:id="@+id/change_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/change_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_change_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/change_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
         <View
             android:id="@+id/finish_secondary_activities_divider"
             android:layout_width="match_parent"
@@ -201,6 +301,14 @@
             android:visibility="gone"
             android:background="#AAAAAA" />
 
+        <Button
+            android:id="@+id/finish_secondary_activities_button"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:layout_centerHorizontal="true"
+            android:text="Finish secondary activities"
+            android:visibility="gone"/>
+
         <View
             android:layout_width="match_parent"
             android:layout_height="1dp"
diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
index c71a541..4d6e260 100644
--- a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
@@ -143,6 +143,106 @@
                 android:text="Swap the position of primary and secondary container" />
         </RadioGroup>
 
+        <!-- Dropdown for animation background -->
+
+        <View
+            android:id="@+id/animation_background_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/animation_background_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_animation_background"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/animation_background_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for open animation transition -->
+
+        <View
+            android:id="@+id/open_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/open_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_open_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/open_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for close animation transition -->
+
+        <View
+            android:id="@+id/close_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/close_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_close_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/close_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
+        <!-- Dropdown for change animation transition -->
+
+        <View
+            android:id="@+id/change_animation_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/change_animation_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_change_animation"
+            android:visibility="gone"/>
+
+        <Spinner
+            android:id="@+id/change_animation_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"/>
+
         <Button
             android:id="@+id/launch_activity_to_side"
             android:layout_width="wrap_content"
diff --git a/window/window-demos/demo/src/main/res/layout/window_state_config_view.xml b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
similarity index 62%
rename from window/window-demos/demo/src/main/res/layout/window_state_config_view.xml
rename to window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
index 6a97d8f..9c10df2 100644
--- a/window/window-demos/demo/src/main/res/layout/window_state_config_view.xml
+++ b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
@@ -15,24 +15,20 @@
   limitations under the License.
   -->
 
-<LinearLayout
+<androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_marginTop="5dp"
-    android:orientation="horizontal">
+    android:layout_height="match_parent">
 
     <TextView
-        android:id="@+id/config_name"
+        android:id="@+id/textView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:textStyle="bold"
-        android:text="@string/window_state_placeholder"/>
+        android:text="Dual Display Presentation"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
 
-    <TextView
-        android:id="@+id/config_value"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/window_state_placeholder"
-        android:layout_marginLeft="10dp"/>
-</LinearLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/layout/window_state_view.xml b/window/window-demos/demo/src/main/res/layout/window_state_view.xml
deleted file mode 100644
index 4a03bbd..0000000
--- a/window/window-demos/demo/src/main/res/layout/window_state_view.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2023 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical">
-
-    <!-- Callback name -->
-    <TextView
-        android:id="@+id/callback_title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textAppearance="@android:style/TextAppearance.Material.Title"
-        android:text="@string/window_state_placeholder"/>
-
-    <!-- Last update timestamp -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/timestamp_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/timestamp_title"/>
-
-    <!-- Application Display rotation -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/application_display_rotation_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/application_display_rotation_title"/>
-
-    <!-- Activity Display rotation -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/activity_display_rotation_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/activity_display_rotation_title"/>
-
-    <!-- Application Display bounds -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/application_display_bounds_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/application_display_bounds_title"/>
-
-    <!-- Activity Display bounds -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/activity_display_bounds_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/activity_display_bounds_title"/>
-
-    <!-- Previous Application Display rotation -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/prev_application_display_rotation_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/prev_application_display_rotation_title"/>
-
-    <!-- Previous Activity Display rotation -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/prev_activity_display_rotation_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/prev_activity_display_rotation_title"/>
-
-    <!-- Previous Application Display bounds -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/prev_application_display_bounds_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/prev_application_display_bounds_title"/>
-
-    <!-- Previous Activity Display bounds -->
-    <androidx.window.demo.coresdk.WindowStateConfigView
-        android:id="@+id/prev_activity_display_bounds_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:configName="@string/prev_activity_display_bounds_title"/>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/res/values/strings.xml b/window/window-demos/demo/src/main/res/values/strings.xml
index 892cecc..7bc15c3 100644
--- a/window/window-demos/demo/src/main/res/values/strings.xml
+++ b/window/window-demos/demo/src/main/res/values/strings.xml
@@ -50,7 +50,10 @@
     <string name="rear_display">Rear Display Mode</string>
     <string name="rear_display_description">Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode</string>
     <string name="current_split_attributes">Current SplitAttributes:</string>>
-    <string name="current_animation_background_color">Current Animation Background Color:</string>>
+    <string name="current_animation_background">Current Animation Background:</string>>
+    <string name="current_open_animation">Current Open Animation:</string>>
+    <string name="current_close_animation">Current Close Animation:</string>>
+    <string name="current_change_animation">Current Change Animation:</string>>
     <string name="test_ime">Test IME</string>
     <string name="test_ime_button_clear">Clear Logs</string>
     <string name="test_ime_button_close">Close Test IME</string>
@@ -61,13 +64,14 @@
     <string name="ime_button_settings">System IME Settings</string>
     <string name="ime_button_switch_default">Switch default IME</string>
     <string name="install_samples_2">Install window-demos:demo-second-app to launch activities from a different UID.</string>
-    <string name="toast_split_not_support">Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED and ensure device supports splits.</string>
+    <string name="toast_split_not_available">This device does not support Activity Embedding.</string>
+    <string name="toast_split_not_support">Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED.</string>
     <string name="latest_configuration_title">Latest Configuration</string>
     <string name="application_display_listener_title">Application DisplayListener#onDisplayChanged</string>
     <string name="activity_display_listener_title">Activity DisplayListener#onDisplayChanged</string>
     <string name="application_configuration_title">Application#onConfigurationChanged</string>
     <string name="activity_configuration_title">Activity#onConfigurationChanged</string>
-    <string name="display_feature_title">WindowInfoTracker</string>
+    <string name="display_feature_title">WindowInfoTracker#windowLayoutInfo</string>
     <string name="timestamp_title">Timestamp:</string>
     <string name="application_display_rotation_title">Application Display Rotation:</string>
     <string name="activity_display_rotation_title">Activity Display Rotation:</string>
@@ -86,4 +90,10 @@
     <string name="choose_split_type">Choose the split type</string>
     <string name="choose_layout_direction">Choose the layout direction</string>
     <string name="show_placeholder_warning">Placeholder Activity may show (again) because "Use a placeholder for A is checked". Clear the check box to expand Activity A to fill the task by either clicking "FINISH SECONDARY ACTIVITIES".</string>
+    <string name="dual_display">Dual Display</string>
+    <string name="dual_display_description">Demo of showing content on the rear and internal displays concurrently</string>
+    <string name="toast_show_overlay_warning">The device does not support overlay features!</string>
+    <string name="overlay_bounds_text">Overlay bounds:</string>
+    <string name="width_in_ratio">Width in ratio: </string>
+    <string name="height_in_ratio">Height in ratio: </string>
 </resources>
diff --git a/window/window-demos/demo/src/main/res/values/styles.xml b/window/window-demos/demo/src/main/res/values/styles.xml
index 5586114..347966d 100644
--- a/window/window-demos/demo/src/main/res/values/styles.xml
+++ b/window/window-demos/demo/src/main/res/values/styles.xml
@@ -15,15 +15,6 @@
   -->
 
 <resources>
-
-    <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
-        <item name="colorPrimary">@color/colorPrimary</item>
-        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
-        <item name="colorAccent">@color/colorAccent</item>
-    </style>
-
     <!-- Theme to show the expanded dialog Activity as transparent. -->
     <style name="ExpandedDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
         <item name="windowNoTitle">true</item>
diff --git a/window/window-demos/demo/src/main/res/xml/main_split_config.xml b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
index 8269c38..3a898ce 100644
--- a/window/window-demos/demo/src/main/res/xml/main_split_config.xml
+++ b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
@@ -41,4 +41,14 @@
         <ActivityFilter
             window:activityName="androidx.window.demo.embedding.SplitImeActivityMain"/>
     </SplitPlaceholderRule>
+
+    <!-- Rules for OverlayActivity -->
+
+    <SplitPairRule
+        window:finishPrimaryWithSecondary="never"
+        window:finishSecondaryWithPrimary="adjacent">
+        <SplitPairFilter
+            window:primaryActivityName="androidx.window.demo.embedding.OverlayActivityA"
+            window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>
+    </SplitPairRule>
 </resources>
\ No newline at end of file
diff --git a/window/window-java/api/current.txt b/window/window-java/api/current.txt
index 2e19128..c3b2d91 100644
--- a/window/window-java/api/current.txt
+++ b/window/window-java/api/current.txt
@@ -11,6 +11,18 @@
 
 package androidx.window.java.embedding {
 
+  public final class ActivityEmbeddingControllerCallbackAdapter {
+    ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+  }
+
+  public final class OverlayControllerCallbackAdapter {
+    ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+  }
+
   @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
     ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
diff --git a/window/window-java/api/restricted_current.txt b/window/window-java/api/restricted_current.txt
index 2e19128..c3b2d91 100644
--- a/window/window-java/api/restricted_current.txt
+++ b/window/window-java/api/restricted_current.txt
@@ -11,6 +11,18 @@
 
 package androidx.window.java.embedding {
 
+  public final class ActivityEmbeddingControllerCallbackAdapter {
+    ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer<androidx.window.embedding.EmbeddedActivityWindowInfo> listener);
+  }
+
+  public final class OverlayControllerCallbackAdapter {
+    ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer<androidx.window.embedding.OverlayInfo> consumer);
+  }
+
   @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
     ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
new file mode 100644
index 0000000..0f0e7f2
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.app.Activity
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.EmbeddedActivityWindowInfo
+import androidx.window.java.core.CallbackToFlowAdapter
+import java.util.concurrent.Executor
+
+/**
+ * An adapted interface for [ActivityEmbeddingController] that provides callback shaped APIs to
+ * report the latest [EmbeddedActivityWindowInfo].
+ *
+ * It should only be used if [ActivityEmbeddingController.embeddedActivityWindowInfo] is not
+ * available. For example, an app is written in Java and cannot use Flow APIs.
+ *
+ * @param controller an [ActivityEmbeddingController] that can be obtained by
+ *   [ActivityEmbeddingController.getInstance].
+ * @constructor creates a callback adapter of
+ *   [ActivityEmbeddingController.embeddedActivityWindowInfo] flow API.
+ */
+class ActivityEmbeddingControllerCallbackAdapter(
+    private val controller: ActivityEmbeddingController
+) {
+    private val callbackToFlowAdapter = CallbackToFlowAdapter()
+
+    /**
+     * Registers a listener for updates of [EmbeddedActivityWindowInfo] of [activity].
+     *
+     * The [listener] will immediately be invoked with the latest value upon registration if the
+     * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
+     *
+     * When the [activity] is embedded, the [listener] will be invoked when
+     * [EmbeddedActivityWindowInfo] is changed. When the [activity] is not embedded, the [listener]
+     * will not be triggered unless the [activity] is becoming non-embedded from embedded.
+     *
+     * Note that this API is only supported on the device with
+     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+     * [WindowSdkExtensions.extensionVersion] is less than 6, this [listener] will not be invoked.
+     *
+     * @param activity the [Activity] that is interested in getting the embedded window info.
+     * @param executor the [Executor] to dispatch the [EmbeddedActivityWindowInfo] change.
+     * @param listener the [Consumer] that will be invoked on the [executor] when there is an update
+     *   to [EmbeddedActivityWindowInfo].
+     */
+    @RequiresWindowSdkExtension(6)
+    fun addEmbeddedActivityWindowInfoListener(
+        activity: Activity,
+        executor: Executor,
+        listener: Consumer<EmbeddedActivityWindowInfo>
+    ) {
+        callbackToFlowAdapter.connect(
+            executor,
+            listener,
+            controller.embeddedActivityWindowInfo(activity)
+        )
+    }
+
+    /**
+     * Unregisters a listener that was previously registered via
+     * [addEmbeddedActivityWindowInfoListener].
+     *
+     * It's no-op if the [listener] has not been registered.
+     *
+     * @param listener the previously registered [Consumer] to unregister.
+     */
+    @RequiresWindowSdkExtension(6)
+    fun removeEmbeddedActivityWindowInfoListener(listener: Consumer<EmbeddedActivityWindowInfo>) {
+        callbackToFlowAdapter.disconnect(listener)
+    }
+}
diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
new file mode 100644
index 0000000..78d9e13
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
+import androidx.window.java.core.CallbackToFlowAdapter
+import java.util.concurrent.Executor
+
+/**
+ * An adapted interface for [OverlayController] that provides callback shaped APIs to report the
+ * latest [OverlayInfo].
+ *
+ * It should only be used if [OverlayController.overlayInfo] is not available. For example, an app
+ * is written in Java and cannot use Flow APIs.
+ *
+ * @param controller an [OverlayController] that can be obtained by [OverlayController.getInstance].
+ * @constructor creates a callback adapter of [OverlayController.overlayInfo] flow API.
+ */
+class OverlayControllerCallbackAdapter(private val controller: OverlayController) {
+
+    private val callbackToFlowAdapter = CallbackToFlowAdapter()
+
+    /**
+     * Registers a listener for updates of [OverlayInfo] that [overlayTag] is associated with.
+     *
+     * If there is no active overlay [ActivityStack], the reported [OverlayInfo.activityStack] and
+     * [OverlayInfo.currentOverlayAttributes] will be `null`.
+     *
+     * Note that launching an overlay [ActivityStack] only supports on the device with
+     * [WindowSdkExtensions.extensionVersion] equal to or larger than 5. If
+     * [WindowSdkExtensions.extensionVersion] is less than 5, this flow will always report
+     * [OverlayInfo] without associated [OverlayInfo.activityStack].
+     *
+     * @param overlayTag the overlay [ActivityStack]'s tag which is set through
+     *   [OverlayCreateParams]
+     * @param executor the [Executor] to dispatch the [OverlayInfo] change
+     * @param consumer the [Consumer] that will be invoked on the [executor] when there is an update
+     *   to [OverlayInfo].
+     */
+    @RequiresWindowSdkExtension(5)
+    fun addOverlayInfoListener(
+        overlayTag: String,
+        executor: Executor,
+        consumer: Consumer<OverlayInfo>
+    ) {
+        callbackToFlowAdapter.connect(executor, consumer, controller.overlayInfo(overlayTag))
+    }
+
+    /**
+     * Unregisters a listener that was previously registered via [addOverlayInfoListener].
+     *
+     * @param consumer the previously registered [Consumer] to unregister.
+     */
+    @RequiresWindowSdkExtension(5)
+    fun removeOverlayInfoListener(consumer: Consumer<OverlayInfo>) {
+        callbackToFlowAdapter.disconnect(consumer)
+    }
+}
diff --git a/window/window-testing/api/current.txt b/window/window-testing/api/current.txt
index 847a9e8..84d25ac 100644
--- a/window/window-testing/api/current.txt
+++ b/window/window-testing/api/current.txt
@@ -1,4 +1,14 @@
 // Signature format: 4.0
+package androidx.window.testing {
+
+  public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
+    ctor public WindowSdkExtensionsRule();
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public void overrideExtensionVersion(@IntRange(from=0L) int version);
+  }
+
+}
+
 package androidx.window.testing.embedding {
 
   public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
diff --git a/window/window-testing/api/restricted_current.txt b/window/window-testing/api/restricted_current.txt
index 847a9e8..84d25ac 100644
--- a/window/window-testing/api/restricted_current.txt
+++ b/window/window-testing/api/restricted_current.txt
@@ -1,4 +1,14 @@
 // Signature format: 4.0
+package androidx.window.testing {
+
+  public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
+    ctor public WindowSdkExtensionsRule();
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public void overrideExtensionVersion(@IntRange(from=0L) int version);
+  }
+
+}
+
 package androidx.window.testing.embedding {
 
   public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
diff --git a/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
new file mode 100644
index 0000000..75ebdf5
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.testing
+
+import androidx.annotation.IntRange
+import androidx.window.WindowSdkExtensions
+
+/**
+ * A fake [WindowSdkExtensions] implementation that can override [extensionVersion], which is
+ * intended to be used during unit tests.
+ */
+internal class FakeWindowSdkExtensions : WindowSdkExtensions() {
+
+    override val extensionVersion: Int
+        get() = _extensionVersion
+
+    private var _extensionVersion: Int = 0
+
+    internal fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+        require(version >= 0) { "The override version must equal to or greater than 0." }
+        _extensionVersion = version
+    }
+}
diff --git a/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
new file mode 100644
index 0000000..185fdae
--- /dev/null
+++ b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.testing
+
+import androidx.annotation.IntRange
+import androidx.window.WindowSdkExtensions
+import androidx.window.WindowSdkExtensionsDecorator
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * [TestRule] for overriding [WindowSdkExtensions] properties in unit tests.
+ *
+ * The [TestRule] is designed to only be used in unit tests. Users should use the actual
+ * [WindowSdkExtensions] properties for instrumentation tests. Overriding the device's extensions
+ * version to a higher version may lead to unexpected test failures or even app crash.
+ */
+class WindowSdkExtensionsRule : TestRule {
+
+    private val fakeWindowSdkExtensions = FakeWindowSdkExtensions()
+
+    override fun apply(
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        base: Statement,
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        description: Description
+    ): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                WindowSdkExtensions.overrideDecorator(
+                    object : WindowSdkExtensionsDecorator {
+                        override fun decorate(
+                            windowSdkExtensions: WindowSdkExtensions
+                        ): WindowSdkExtensions = fakeWindowSdkExtensions
+                    }
+                )
+                try {
+                    base.evaluate()
+                } finally {
+                    WindowSdkExtensions.reset()
+                }
+            }
+        }
+    }
+
+    /**
+     * Overrides the [WindowSdkExtensions.extensionVersion] for testing.
+     *
+     * @param version The extension version to override.
+     */
+    fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
+        fakeWindowSdkExtensions.overrideExtensionVersion(version)
+    }
+}
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
index 7c62b4f..e0c3480 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
@@ -18,9 +18,6 @@
 package androidx.window.testing.embedding
 
 import android.app.Activity
-import android.os.Binder
-import androidx.annotation.RestrictTo
-import androidx.annotation.VisibleForTesting
 import androidx.window.embedding.ActivityStack
 
 /**
@@ -41,8 +38,3 @@
     activitiesInProcess: List<Activity> = emptyList(),
     isEmpty: Boolean = false,
 ): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@VisibleForTesting
-@JvmField
-val TEST_ACTIVITY_STACK_TOKEN = Binder()
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
index 5195f28..2cabe03 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
@@ -17,7 +17,6 @@
 
 package androidx.window.testing.embedding
 
-import android.os.Binder
 import androidx.window.embedding.ActivityStack
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitInfo
@@ -44,6 +43,8 @@
     secondActivityStack: ActivityStack = TestActivityStack(),
     splitAttributes: SplitAttributes = SplitAttributes.Builder().build(),
 ): SplitInfo =
-    SplitInfo(primaryActivityStack, secondActivityStack, splitAttributes, TEST_SPLIT_INFO_TOKEN)
-
-private val TEST_SPLIT_INFO_TOKEN = Binder()
+    SplitInfo(
+        primaryActivityStack,
+        secondActivityStack,
+        splitAttributes,
+    )
diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
index 473e3d9..dcd3a23 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
@@ -17,17 +17,24 @@
 package androidx.window.testing.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
-import android.os.IBinder
+import android.os.Bundle
 import androidx.core.util.Consumer
+import androidx.window.core.ExperimentalWindowApi
 import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.EmbeddedActivityWindowInfo
 import androidx.window.embedding.EmbeddingBackend
+import androidx.window.embedding.EmbeddingConfiguration
 import androidx.window.embedding.EmbeddingRule
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayAttributesCalculatorParams
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.OverlayInfo
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitAttributesCalculatorParams
 import androidx.window.embedding.SplitController
 import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
 import androidx.window.embedding.SplitInfo
+import androidx.window.embedding.SplitPinRule
 import java.util.concurrent.Executor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -149,6 +156,15 @@
     override fun isActivityEmbedded(activity: Activity): Boolean =
         embeddedActivities.contains(activity)
 
+    @OptIn(ExperimentalWindowApi::class)
+    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun unpinTopActivityStack(taskId: Int) {
+        TODO("Not yet implemented")
+    }
+
     override fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     ) {
@@ -164,13 +180,29 @@
     }
 
     override fun setLaunchingActivityStack(
-        options: ActivityOptions,
-        token: IBinder
-    ): ActivityOptions {
+        options: Bundle,
+        activityStack: ActivityStack,
+    ): Bundle {
         TODO("Not yet implemented")
     }
 
-    override fun invalidateTopVisibleSplitAttributes() {
+    override fun setOverlayCreateParams(
+        options: Bundle,
+        overlayCreateParams: OverlayCreateParams
+    ): Bundle {
+        TODO("Not yet implemented")
+    }
+
+    override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+        TODO("Not yet implemented")
+    }
+
+    @OptIn(ExperimentalWindowApi::class)
+    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+        TODO("Not yet implemented")
+    }
+
+    override fun invalidateVisibleActivityStacks() {
         TODO("Not yet implemented")
     }
 
@@ -178,6 +210,45 @@
         TODO("Not yet implemented")
     }
 
+    override fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun clearOverlayAttributesCalculator() {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: Consumer<OverlayInfo>
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+        activity: Activity,
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    ) {
+        TODO("Not yet implemented")
+    }
+
     private fun validateRules(rules: Set<EmbeddingRule>) {
         val tags = HashSet<String>()
         rules.forEach { rule ->
diff --git a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
index f59e5ae..5d7d07a 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
@@ -39,13 +39,13 @@
     override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
         val displayMetrics = activity.resources.displayMetrics
         val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
-        return WindowMetrics(bounds)
+        return WindowMetrics(bounds, density = displayMetrics.density)
     }
 
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
         val displayMetrics = activity.resources.displayMetrics
         val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
-        return WindowMetrics(bounds)
+        return WindowMetrics(bounds, density = displayMetrics.density)
     }
 
     // WindowManager#getDefaultDisplay is deprecated but we have this for compatibility with
@@ -53,9 +53,10 @@
     @Suppress("DEPRECATION")
     override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
         val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        val density = context.resources.displayMetrics.density
 
         return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-            Api30Impl.getWindowMetrics(wm)
+            Api30Impl.getWindowMetrics(wm, context)
         } else {
             val displaySize = Point()
             // We use getRealSize instead of getSize here because:
@@ -67,7 +68,7 @@
             //      getRealSize.
             wm.defaultDisplay.getRealSize(displaySize)
             val bounds = Rect(0, 0, displaySize.x, displaySize.y)
-            WindowMetrics(bounds)
+            WindowMetrics(bounds, density = density)
         }
     }
 
@@ -77,8 +78,14 @@
 
     @RequiresApi(Build.VERSION_CODES.R)
     private object Api30Impl {
-        fun getWindowMetrics(windowManager: WindowManager): WindowMetrics {
-            return WindowMetrics(windowManager.currentWindowMetrics.bounds)
+        fun getWindowMetrics(
+            windowManager: WindowManager,
+            @UiContext context: Context
+        ): WindowMetrics {
+            return WindowMetrics(
+                windowManager.currentWindowMetrics.bounds,
+                density = context.resources.displayMetrics.density
+            )
         }
     }
 }
diff --git a/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
new file mode 100644
index 0000000..24dc179
--- /dev/null
+++ b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.testing
+
+import androidx.window.WindowSdkExtensions
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+/** Test class to verify [WindowSdkExtensionsRule] behaviors. */
+class WindowSdkExtensionsRuleTest {
+
+    @JvmField @Rule val rule = WindowSdkExtensionsRule()
+
+    /** Verifies the [WindowSdkExtensionsRule] behavior. */
+    @Test
+    fun testWindowSdkExtensionsRule() {
+        assertEquals(
+            "The WindowSdkExtensions.extensionVersion is 0 in unit test",
+            0,
+            WindowSdkExtensions.getInstance().extensionVersion
+        )
+
+        rule.overrideExtensionVersion(3)
+
+        assertEquals(3, WindowSdkExtensions.getInstance().extensionVersion)
+    }
+}
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
index ca3d9c4..d4df0a3 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
@@ -40,8 +40,10 @@
     public void testActivityStackDefaultValue() {
         final ActivityStack activityStack = TestActivityStack.createTestActivityStack();
 
-        assertEquals(new ActivityStack(Collections.emptyList(), false /* isEmpty */),
-                activityStack);
+        assertEquals(
+                new ActivityStack(Collections.emptyList(), false /* isEmpty */),
+                activityStack
+        );
     }
 
     /** Verifies {@link TestActivityStack} */
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
index 27e70c26..a797045 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
@@ -52,7 +52,7 @@
 public class SplitAttributesCalculatorParamsTestingJavaTest {
     private static final Rect TEST_BOUNDS = new Rect(0, 0, 2000, 2000);
     private static final WindowMetrics TEST_METRICS = new WindowMetrics(TEST_BOUNDS,
-            WindowInsetsCompat.CONSUMED);
+            WindowInsetsCompat.CONSUMED, 1f /* density */);
     private static final SplitAttributes DEFAULT_SPLIT_ATTRIBUTES =
             new SplitAttributes.Builder().build();
     private static final SplitAttributes TABLETOP_HINGE_ATTRIBUTES = new SplitAttributes.Builder()
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
index 2e90c35..1a4eed3 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
@@ -102,7 +102,7 @@
 
     companion object {
         private val TEST_BOUNDS = Rect(0, 0, 2000, 2000)
-        private val TEST_METRICS = WindowMetrics(TEST_BOUNDS)
+        private val TEST_METRICS = WindowMetrics(TEST_BOUNDS, density = 1f)
         private val DEFAULT_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
         private val TABLETOP_HINGE_ATTRIBUTES =
             SplitAttributes.Builder()
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
index f005d0e..ef2f690 100644
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
+++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
@@ -16,14 +16,12 @@
 
 package androidx.window.testing.embedding
 
-import androidx.window.core.ExperimentalWindowApi
 import androidx.window.embedding.SplitAttributes
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.mockito.kotlin.mock
 
 /** Test class to verify [TestSplitInfo] */
-@OptIn(ExperimentalWindowApi::class)
 class SplitInfoTestingTest {
 
     /** Verifies the default value of [TestSplitInfo]. */
diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
deleted file mode 100644
index ca6efd7..0000000
--- a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.testing.embedding
-
-import android.app.Activity
-import android.os.Binder
-import android.os.IBinder
-import androidx.window.core.ExperimentalWindowApi
-import androidx.window.embedding.ActivityStack
-import androidx.window.embedding.SplitAttributes
-import androidx.window.embedding.SplitInfo
-
-/**
- * A convenience method to get a test [SplitInfo] with default values provided. With the default
- * values it returns an empty [ActivityStack] for the primary and secondary stacks. The default
- * [SplitAttributes] are for splitting equally and matching the locale layout.
- *
- * Note: This method should be used for testing local logic as opposed to end to end verification.
- * End to end verification requires a device that supports Activity Embedding.
- *
- * @param primaryActivity the [Activity] for the primary container.
- * @param secondaryActivity the [Activity] for the secondary container.
- * @param splitAttributes the [SplitAttributes].
- */
-@ExperimentalWindowApi
-fun TestSplitInfo(
-    primaryActivity: Activity,
-    secondaryActivity: Activity,
-    splitAttributes: SplitAttributes = SplitAttributes(),
-    token: IBinder = Binder()
-): SplitInfo {
-    val primaryActivityStack = TestActivityStack(primaryActivity, false)
-    val secondaryActivityStack = TestActivityStack(secondaryActivity, false)
-    return SplitInfo(primaryActivityStack, secondaryActivityStack, splitAttributes, token)
-}
-
-/**
- * A convenience method to get a test [ActivityStack] with default values provided. With the default
- * values, there will be a single [Activity] in the stack and it will be considered not empty.
- *
- * Note: This method should be used for testing local logic as opposed to end to end verification.
- * End to end verification requires a device that supports Activity Embedding.
- *
- * @param testActivity an [Activity] that should be considered in the stack
- * @param isEmpty states if the stack is empty or not. In practice an [ActivityStack] with a single
- *   [Activity] but [isEmpty] set to `false` means there is an [Activity] from outside the process
- *   in the stack.
- */
-@ExperimentalWindowApi
-fun TestActivityStack(
-    testActivity: Activity,
-    isEmpty: Boolean = true,
-): ActivityStack {
-    return ActivityStack(
-        listOf(testActivity),
-        isEmpty,
-    )
-}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index f68c635..ff947e3 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -13,6 +13,8 @@
     field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
     field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
+    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
   }
 
   public abstract class WindowSdkExtensions {
@@ -107,8 +109,10 @@
 
   @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
     method public android.content.Context getContext();
+    method public android.view.Window? getWindow();
     method public void setContentView(android.view.View view);
     property public abstract android.content.Context context;
+    property public abstract android.view.Window? window;
   }
 
 }
@@ -123,9 +127,13 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.EmbeddedActivityWindowInfo> embeddedActivityWindowInfo(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
+    method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
     method public boolean isActivityEmbedded(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -133,6 +141,10 @@
     method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
   }
 
+  public final class ActivityEmbeddingOptions {
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+  }
+
   public final class ActivityFilter {
     ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
     method public android.content.ComponentName getComponentName();
@@ -163,6 +175,121 @@
     property public final boolean isEmpty;
   }
 
+  public abstract class DividerAttributes {
+    method public final int getColor();
+    method public final int getWidthDp();
+    property public final int color;
+    property public final int widthDp;
+    field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
+    field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
+    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+  }
+
+  public static final class DividerAttributes.Companion {
+  }
+
+  public abstract static class DividerAttributes.DragRange {
+    field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
+    field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
+  }
+
+  public static final class DividerAttributes.DragRange.Companion {
+  }
+
+  public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
+    ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
+    method public float getMaxRatio();
+    method public float getMinRatio();
+    property public final float maxRatio;
+    property public final float minRatio;
+  }
+
+  public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
+    method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
+    method public boolean isDraggingToFullscreenAllowed();
+    property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
+    property public final boolean isDraggingToFullscreenAllowed;
+  }
+
+  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
+    ctor public DividerAttributes.DraggableDividerAttributes.Builder();
+    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
+    method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDraggingToFullscreenAllowed(boolean allowed);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+  }
+
+  public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
+  }
+
+  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
+    ctor public DividerAttributes.FixedDividerAttributes.Builder();
+    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+  }
+
+  public final class EmbeddedActivityWindowInfo {
+    method public android.graphics.Rect getBoundsInParentHost();
+    method public android.graphics.Rect getParentHostBounds();
+    method public boolean isEmbedded();
+    property public final android.graphics.Rect boundsInParentHost;
+    property public final boolean isEmbedded;
+    property public final android.graphics.Rect parentHostBounds;
+  }
+
+  public abstract class EmbeddingAnimationBackground {
+    method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+    field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
+  }
+
+  public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
+    method public int getColor();
+    property public final int color;
+  }
+
+  public static final class EmbeddingAnimationBackground.Companion {
+    method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+  }
+
+  public final class EmbeddingAnimationParams {
+    ctor public EmbeddingAnimationParams();
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation);
+    method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getChangeAnimation();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getCloseAnimation();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getOpenAnimation();
+    property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation;
+  }
+
+  public static final class EmbeddingAnimationParams.AnimationSpec {
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec DEFAULT;
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec JUMP_CUT;
+  }
+
+  public static final class EmbeddingAnimationParams.AnimationSpec.Companion {
+  }
+
+  public static final class EmbeddingAnimationParams.Builder {
+    ctor public EmbeddingAnimationParams.Builder();
+    method public androidx.window.embedding.EmbeddingAnimationParams build();
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setChangeAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setCloseAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setOpenAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+  }
+
   public final class EmbeddingAspectRatio {
     method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
     field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
@@ -174,6 +301,29 @@
     method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
   }
 
+  public final class EmbeddingConfiguration {
+    ctor public EmbeddingConfiguration();
+    ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
+    method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
+    property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
+  }
+
+  public static final class EmbeddingConfiguration.Builder {
+    ctor public EmbeddingConfiguration.Builder();
+    method public androidx.window.embedding.EmbeddingConfiguration build();
+    method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
+  }
+
+  public static final class EmbeddingConfiguration.DimAreaBehavior {
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
+  }
+
+  public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
+  }
+
   public abstract class EmbeddingRule {
     method public final String? getTag();
     property public final String? tag;
@@ -196,8 +346,21 @@
   }
 
   public final class SplitAttributes {
+    ctor public SplitAttributes();
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
+    ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+    ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground, androidx.window.embedding.DividerAttributes dividerAttributes);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams, optional androidx.window.embedding.DividerAttributes dividerAttributes);
+    method @Deprecated public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+    method public androidx.window.embedding.EmbeddingAnimationParams getAnimationParams();
+    method public androidx.window.embedding.DividerAttributes getDividerAttributes();
     method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
     method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+    property @Deprecated public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+    property public final androidx.window.embedding.EmbeddingAnimationParams animationParams;
+    property public final androidx.window.embedding.DividerAttributes dividerAttributes;
     property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
     property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
     field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
@@ -206,6 +369,9 @@
   public static final class SplitAttributes.Builder {
     ctor public SplitAttributes.Builder();
     method public androidx.window.embedding.SplitAttributes build();
+    method @Deprecated @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+    method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.embedding.EmbeddingAnimationParams params);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
     method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
     method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
   }
@@ -256,10 +422,11 @@
     method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
     method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
+    method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
@@ -326,6 +493,24 @@
     method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
   }
 
+  public final class SplitPinRule extends androidx.window.embedding.SplitRule {
+    method public boolean isSticky();
+    property public final boolean isSticky;
+  }
+
+  public static final class SplitPinRule.Builder {
+    ctor public SplitPinRule.Builder();
+    method public androidx.window.embedding.SplitPinRule build();
+    method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
+    method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
+  }
+
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
@@ -431,10 +616,20 @@
   public static final class FoldingFeature.State.Companion {
   }
 
+  public final class SupportedPosture {
+    field public static final androidx.window.layout.SupportedPosture.Companion Companion;
+    field public static final androidx.window.layout.SupportedPosture TABLETOP;
+  }
+
+  public static final class SupportedPosture.Companion {
+  }
+
   public interface WindowInfoTracker {
     method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+    method public default java.util.List<androidx.window.layout.SupportedPosture> getSupportedPostures();
     method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
     method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
+    property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List<androidx.window.layout.SupportedPosture> supportedPostures;
     field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
   }
 
@@ -449,8 +644,10 @@
 
   public final class WindowMetrics {
     method public android.graphics.Rect getBounds();
+    method public float getDensity();
     method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
     property public final android.graphics.Rect bounds;
+    property public final float density;
   }
 
   public interface WindowMetricsCalculator {
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index f68c635..5b2213b 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -13,6 +13,8 @@
     field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
     field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
     field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
+    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
+    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
   }
 
   public abstract class WindowSdkExtensions {
@@ -107,8 +109,10 @@
 
   @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
     method public android.content.Context getContext();
+    method public android.view.Window? getWindow();
     method public void setContentView(android.view.View view);
     property public abstract android.content.Context context;
+    property public abstract android.view.Window? window;
   }
 
 }
@@ -123,9 +127,13 @@
 package androidx.window.embedding {
 
   public final class ActivityEmbeddingController {
-    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.EmbeddedActivityWindowInfo> embeddedActivityWindowInfo(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set<androidx.window.embedding.ActivityStack> activityStacks);
+    method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
     method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
+    method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
     method public boolean isActivityEmbedded(android.app.Activity activity);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
     field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
   }
 
@@ -133,6 +141,11 @@
     method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
   }
 
+  public final class ActivityEmbeddingOptions {
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);
+  }
+
   public final class ActivityFilter {
     ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
     method public android.content.ComponentName getComponentName();
@@ -163,6 +176,121 @@
     property public final boolean isEmpty;
   }
 
+  public abstract class DividerAttributes {
+    method public final int getColor();
+    method public final int getWidthDp();
+    property public final int color;
+    property public final int widthDp;
+    field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
+    field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
+    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
+  }
+
+  public static final class DividerAttributes.Companion {
+  }
+
+  public abstract static class DividerAttributes.DragRange {
+    field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
+    field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
+  }
+
+  public static final class DividerAttributes.DragRange.Companion {
+  }
+
+  public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
+    ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
+    method public float getMaxRatio();
+    method public float getMinRatio();
+    property public final float maxRatio;
+    property public final float minRatio;
+  }
+
+  public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
+    method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
+    method public boolean isDraggingToFullscreenAllowed();
+    property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
+    property public final boolean isDraggingToFullscreenAllowed;
+  }
+
+  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
+    ctor public DividerAttributes.DraggableDividerAttributes.Builder();
+    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
+    method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDraggingToFullscreenAllowed(boolean allowed);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+  }
+
+  public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
+  }
+
+  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
+    ctor public DividerAttributes.FixedDividerAttributes.Builder();
+    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
+  }
+
+  public final class EmbeddedActivityWindowInfo {
+    method public android.graphics.Rect getBoundsInParentHost();
+    method public android.graphics.Rect getParentHostBounds();
+    method public boolean isEmbedded();
+    property public final android.graphics.Rect boundsInParentHost;
+    property public final boolean isEmbedded;
+    property public final android.graphics.Rect parentHostBounds;
+  }
+
+  public abstract class EmbeddingAnimationBackground {
+    method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+    field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
+  }
+
+  public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
+    method public int getColor();
+    property public final int color;
+  }
+
+  public static final class EmbeddingAnimationBackground.Companion {
+    method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
+  }
+
+  public final class EmbeddingAnimationParams {
+    ctor public EmbeddingAnimationParams();
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation);
+    ctor public EmbeddingAnimationParams(optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation, optional androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation);
+    method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getChangeAnimation();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getCloseAnimation();
+    method public androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec getOpenAnimation();
+    property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec changeAnimation;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec closeAnimation;
+    property public final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec openAnimation;
+  }
+
+  public static final class EmbeddingAnimationParams.AnimationSpec {
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec DEFAULT;
+    field public static final androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec JUMP_CUT;
+  }
+
+  public static final class EmbeddingAnimationParams.AnimationSpec.Companion {
+  }
+
+  public static final class EmbeddingAnimationParams.Builder {
+    ctor public EmbeddingAnimationParams.Builder();
+    method public androidx.window.embedding.EmbeddingAnimationParams build();
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setChangeAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setCloseAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+    method public androidx.window.embedding.EmbeddingAnimationParams.Builder setOpenAnimation(androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec spec);
+  }
+
   public final class EmbeddingAspectRatio {
     method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
     field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
@@ -174,11 +302,149 @@
     method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class EmbeddingBounds {
+    ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);
+    method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();
+    method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();
+    method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();
+    property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;
+    property public final androidx.window.embedding.EmbeddingBounds.Dimension height;
+    property public final androidx.window.embedding.EmbeddingBounds.Dimension width;
+    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;
+    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;
+    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;
+    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;
+    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;
+    field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;
+  }
+
+  public static final class EmbeddingBounds.Alignment {
+    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;
+    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;
+    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;
+    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;
+    field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;
+  }
+
+  public static final class EmbeddingBounds.Alignment.Companion {
+  }
+
+  public static final class EmbeddingBounds.Companion {
+  }
+
+  public abstract static class EmbeddingBounds.Dimension {
+    method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
+    method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
+    field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;
+    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;
+  }
+
+  public static final class EmbeddingBounds.Dimension.Companion {
+    method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
+    method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
+  }
+
+  public final class EmbeddingConfiguration {
+    ctor public EmbeddingConfiguration();
+    ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
+    method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
+    property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
+  }
+
+  public static final class EmbeddingConfiguration.Builder {
+    ctor public EmbeddingConfiguration.Builder();
+    method public androidx.window.embedding.EmbeddingConfiguration build();
+    method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
+  }
+
+  public static final class EmbeddingConfiguration.DimAreaBehavior {
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
+    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
+  }
+
+  public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
+  }
+
   public abstract class EmbeddingRule {
     method public final String? getTag();
     property public final String? tag;
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayAttributes {
+    ctor public OverlayAttributes();
+    ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);
+    method public androidx.window.embedding.EmbeddingBounds getBounds();
+    property public final androidx.window.embedding.EmbeddingBounds bounds;
+  }
+
+  public static final class OverlayAttributes.Builder {
+    ctor public OverlayAttributes.Builder();
+    method public androidx.window.embedding.OverlayAttributes build();
+    method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayAttributesCalculatorParams {
+    method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();
+    method public String getOverlayTag();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+    property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;
+    property public final String overlayTag;
+    property public final android.content.res.Configuration parentConfiguration;
+    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayController {
+    method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void clearOverlayAttributesCalculator();
+    method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);
+    method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public kotlinx.coroutines.flow.Flow<androidx.window.embedding.OverlayInfo> overlayInfo(String overlayTag);
+    method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.OverlayAttributesCalculatorParams,androidx.window.embedding.OverlayAttributes> calculator);
+    method @androidx.window.RequiresWindowSdkExtension(version=androidx.window.embedding.OverlayController.OVERLAY_FEATURE_VERSION) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);
+    field public static final androidx.window.embedding.OverlayController.Companion Companion;
+  }
+
+  public static final class OverlayController.Companion {
+    method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayCreateParams {
+    ctor public OverlayCreateParams();
+    ctor public OverlayCreateParams(optional String tag);
+    ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);
+    method public static String generateOverlayTag();
+    method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();
+    method public String getTag();
+    property public final androidx.window.embedding.OverlayAttributes overlayAttributes;
+    property public final String tag;
+    field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;
+  }
+
+  public static final class OverlayCreateParams.Builder {
+    ctor public OverlayCreateParams.Builder();
+    method public androidx.window.embedding.OverlayCreateParams build();
+    method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);
+    method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);
+  }
+
+  public static final class OverlayCreateParams.Companion {
+    method public String generateOverlayTag();
+  }
+
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class OverlayInfo {
+    method public operator boolean contains(android.app.Activity activity);
+    method public androidx.window.embedding.ActivityStack? getActivityStack();
+    method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();
+    method public String getOverlayTag();
+    property public final androidx.window.embedding.ActivityStack? activityStack;
+    property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;
+    property public final String overlayTag;
+  }
+
   public final class RuleController {
     method public void addRule(androidx.window.embedding.EmbeddingRule rule);
     method public void clearRules();
@@ -196,8 +462,21 @@
   }
 
   public final class SplitAttributes {
+    ctor public SplitAttributes();
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
+    ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
+    ctor @Deprecated public SplitAttributes(androidx.window.embedding.SplitAttributes.SplitType splitType, androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, androidx.window.embedding.EmbeddingAnimationBackground animationBackground, androidx.window.embedding.DividerAttributes dividerAttributes);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams);
+    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationParams animationParams, optional androidx.window.embedding.DividerAttributes dividerAttributes);
+    method @Deprecated public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
+    method public androidx.window.embedding.EmbeddingAnimationParams getAnimationParams();
+    method public androidx.window.embedding.DividerAttributes getDividerAttributes();
     method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
     method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+    property @Deprecated public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
+    property public final androidx.window.embedding.EmbeddingAnimationParams animationParams;
+    property public final androidx.window.embedding.DividerAttributes dividerAttributes;
     property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
     property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
     field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
@@ -206,6 +485,9 @@
   public static final class SplitAttributes.Builder {
     ctor public SplitAttributes.Builder();
     method public androidx.window.embedding.SplitAttributes build();
+    method @Deprecated @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
+    method @androidx.window.RequiresWindowSdkExtension(version=7) public androidx.window.embedding.SplitAttributes.Builder setAnimationParams(androidx.window.embedding.EmbeddingAnimationParams params);
+    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
     method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
     method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
   }
@@ -256,10 +538,11 @@
     method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
     method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
-    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
     method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1<? super androidx.window.embedding.SplitAttributesCalculatorParams,androidx.window.embedding.SplitAttributes> calculator);
     method public kotlinx.coroutines.flow.Flow<java.util.List<androidx.window.embedding.SplitInfo>> splitInfoList(android.app.Activity activity);
-    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
+    method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
+    method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
     property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
@@ -326,6 +609,24 @@
     method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
   }
 
+  public final class SplitPinRule extends androidx.window.embedding.SplitRule {
+    method public boolean isSticky();
+    property public final boolean isSticky;
+  }
+
+  public static final class SplitPinRule.Builder {
+    ctor public SplitPinRule.Builder();
+    method public androidx.window.embedding.SplitPinRule build();
+    method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
+    method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
+    method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
+  }
+
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
@@ -431,10 +732,20 @@
   public static final class FoldingFeature.State.Companion {
   }
 
+  public final class SupportedPosture {
+    field public static final androidx.window.layout.SupportedPosture.Companion Companion;
+    field public static final androidx.window.layout.SupportedPosture TABLETOP;
+  }
+
+  public static final class SupportedPosture.Companion {
+  }
+
   public interface WindowInfoTracker {
     method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+    method public default java.util.List<androidx.window.layout.SupportedPosture> getSupportedPostures();
     method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
     method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
+    property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List<androidx.window.layout.SupportedPosture> supportedPostures;
     field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
   }
 
@@ -449,8 +760,10 @@
 
   public final class WindowMetrics {
     method public android.graphics.Rect getBounds();
+    method public float getDensity();
     method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
     property public final android.graphics.Rect bounds;
+    property public final float density;
   }
 
   public interface WindowMetricsCalculator {
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 90908f3..63085d7 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -54,13 +54,12 @@
     implementation("androidx.core:core:1.8.0")
 
     def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
-    def extensions_version = "androidx.window.extensions:extensions:1.2.0"
+    def extensions_version = project(":window:extensions:extensions")
     // A compile only dependency on extnensions.core so that other libraries do not expose it
     // transitively.
     compileOnly(extensions_core_version)
-    // A compile only dependency on extnensions.core so that other libraries do not expose it
-    // transitively. The testCompile is added because tests are not getting the dependency
-    // transitively.
+    // Test implementation is required since extensions:core is on device. So it is required to
+    // import it in some form. For the library it will be available on device.
     testImplementation(extensions_core_version)
     // A compile only dependency on extnensions.core so that other libraries do not expose it
     // transitively. The androidTestCompile is added because tests are not getting the dependency
@@ -79,7 +78,6 @@
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation(extensions_version)
     testImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    testImplementation(compileOnly(extensions_version))
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.kotlinTestJunit)
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
new file mode 100644
index 0000000..615adab
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.samples.embedding
+
+import android.app.Activity
+import androidx.annotation.Sampled
+import androidx.window.embedding.ActivityEmbeddingController
+import androidx.window.embedding.SplitController
+
+@Sampled
+suspend fun expandPrimaryContainer() {
+    SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity).collect {
+        splitInfoList ->
+        // Find all associated secondary ActivityStacks
+        val associatedSecondaryActivityStacks =
+            splitInfoList.mapTo(mutableSetOf()) { splitInfo -> splitInfo.secondaryActivityStack }
+        // Finish them all.
+        ActivityEmbeddingController.getInstance(primaryActivity)
+            .finishActivityStacks(associatedSecondaryActivityStacks)
+    }
+}
+
+val primaryActivity = Activity()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
new file mode 100644
index 0000000..be84e59
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.samples.embedding
+
+import android.app.Activity
+import androidx.annotation.Sampled
+import androidx.core.app.ActivityOptionsCompat
+import androidx.window.embedding.ActivityStack
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.SplitController
+import androidx.window.embedding.setLaunchingActivityStack
+
+@Sampled
+suspend fun launchingOnPrimaryActivityStack() {
+    var primaryActivityStack: ActivityStack? = null
+
+    SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity).collect {
+        splitInfoList ->
+        primaryActivityStack = splitInfoList.last().primaryActivityStack
+    }
+
+    primaryActivity.startActivity(
+        INTENT,
+        ActivityOptionsCompat.makeBasic()
+            .toBundle()!!
+            .setLaunchingActivityStack(primaryActivity, primaryActivityStack!!)
+    )
+}
+
+@Sampled
+suspend fun launchingOnOverlayActivityStack() {
+    var overlayActivityStack: ActivityStack? = null
+
+    OverlayController.getInstance(context).overlayInfo(TAG_OVERLAY).collect { overlayInfo ->
+        overlayActivityStack = overlayInfo.activityStack
+    }
+
+    // The use case is to launch an Activity to an existing overlay ActivityStack from the overlain
+    // Activity. If activityStack is not specified, the activity is launched to the top of the
+    // host task behind the overlay ActivityStack.
+    overlainActivity.startActivity(
+        INTENT,
+        ActivityOptionsCompat.makeBasic()
+            .toBundle()!!
+            .setLaunchingActivityStack(overlainActivity, overlayActivityStack!!)
+    )
+}
+
+const val TAG_OVERLAY = "overlay"
+val overlainActivity = Activity()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
new file mode 100644
index 0000000..b4e9b12
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.samples.embedding
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Rect
+import androidx.annotation.Sampled
+import androidx.core.app.ActivityOptionsCompat
+import androidx.window.embedding.EmbeddingBounds
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
+import androidx.window.embedding.OverlayAttributes
+import androidx.window.embedding.OverlayController
+import androidx.window.embedding.OverlayCreateParams
+import androidx.window.embedding.setOverlayCreateParams
+
+@Sampled
+fun launchOverlayActivityStackSample() {
+    // Creates an overlay container on the right
+    val params =
+        OverlayCreateParams(
+            overlayAttributes =
+                OverlayAttributes(
+                    EmbeddingBounds(
+                        alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
+                        width = ratio(0.5f),
+                        height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+                    )
+                )
+        )
+
+    val optionsWithOverlayParams =
+        ActivityOptionsCompat.makeBasic()
+            .toBundle()
+            ?.setOverlayCreateParams(launchingActivity, params)
+
+    // Start INTENT to the overlay container specified by params.
+    launchingActivity.startActivity(INTENT, optionsWithOverlayParams)
+}
+
+@Sampled
+fun overlayAttributesCalculatorSample() {
+    // A sample to show overlay on the bottom if the device is portrait, and on the right when
+    // the device is landscape.
+    OverlayController.getInstance(launchingActivity).setOverlayAttributesCalculator { params ->
+        val taskBounds = params.parentWindowMetrics.bounds
+        return@setOverlayAttributesCalculator OverlayAttributes(
+            if (taskBounds.isPortrait()) {
+                EmbeddingBounds(
+                    alignment = EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+                    width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+                    height = ratio(0.5f),
+                )
+            } else {
+                EmbeddingBounds(
+                    alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
+                    width = ratio(0.5f),
+                    height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+                )
+            }
+        )
+    }
+}
+
+private fun Rect.isPortrait(): Boolean = height() >= width()
+
+val launchingActivity: Activity = Activity()
+
+val INTENT = Intent()
diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
index ecf1f67..7126476 100644
--- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -17,7 +17,10 @@
 package androidx.window.samples.embedding
 
 import android.app.Application
+import android.graphics.Color
 import androidx.annotation.Sampled
+import androidx.window.embedding.EmbeddingAnimationBackground
+import androidx.window.embedding.EmbeddingAnimationParams
 import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
@@ -57,6 +60,16 @@
                         SplitAttributes.LayoutDirection.LOCALE
                     }
                 )
+                // Optionally set the animation background and change transition animation to use
+                // when switching between vertical and horizontal
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+                        )
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         }
         return@setSplitAttributesCalculator if (
@@ -66,6 +79,16 @@
             SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_EQUAL)
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                // Optionally set the animation background and change transition animation to use
+                // when switching between vertical and horizontal
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+                        )
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         } else {
             // Expand containers if the device is in portrait or the width is less than 600 dp.
@@ -84,12 +107,30 @@
         return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) {
             builder
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
-                // Set the color to use when switching between vertical and horizontal
+                // Optionally set the animation background and change transition animation to use
+                // when switching between vertical and horizontal
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+                        )
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         } else if (parentConfiguration.screenHeightDp >= 600) {
             builder
                 .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
-                // Set the color to use when switching between vertical and horizontal
+                // Optionally set the animation background and change transition animation to use
+                // when switching between vertical and horizontal
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GRAY)
+                        )
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         } else {
             // Fallback to expand the secondary container
diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
index e0d60f16..2393bc6 100644
--- a/window/window/src/androidTest/AndroidManifest.xml
+++ b/window/window/src/androidTest/AndroidManifest.xml
@@ -42,5 +42,13 @@
             android:name=
                 "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
             android:value="false" />
+        <property
+            android:name=
+                "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+            android:value="false" />
+        <property
+            android:name=
+                "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
+            android:value="false" />
     </application>
 </manifest>
diff --git a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
index 1995125..54c87a3 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
@@ -119,6 +119,42 @@
         }
     }
 
+    @Test
+    fun test_property_allow_user_aspect_ratio_override() {
+        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            // No-op, but to suppress lint
+            return
+        }
+        activityRule.scenario.onActivity { activity ->
+            // Should be false as defined in AndroidManifest.xml
+            assertFalse(
+                getProperty(
+                    activity,
+                    WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE
+                )
+            )
+        }
+    }
+
+    @Test
+    fun test_property_allow_user_aspect_ratio_fullscreen_override() {
+        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            // No-op, but to suppress lint
+            return
+        }
+        activityRule.scenario.onActivity { activity ->
+            // Should be false as defined in AndroidManifest.xml
+            assertFalse(
+                getProperty(
+                    activity,
+                    WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE
+                )
+            )
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.S)
     @Throws(PackageManager.NameNotFoundException::class)
     private fun getProperty(context: Context, propertyName: String): Boolean {
diff --git a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
index ca278ee..6199b64 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
+++ b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
@@ -1,5 +1,6 @@
 package androidx.window
 
+import android.app.Activity
 import android.app.Application
 import android.content.Context
 import android.hardware.display.DisplayManager
@@ -7,7 +8,10 @@
 import android.view.Display
 import android.view.WindowManager
 import androidx.annotation.RequiresApi
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
 import org.junit.Assume.assumeTrue
 
 open class WindowTestUtils {
@@ -28,17 +32,59 @@
                 )
         }
 
-        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
         fun assumeAtLeastVendorApiLevel(min: Int) {
-            val version = WindowSdkExtensions.getInstance().extensionVersion
-            assumeTrue(version >= min)
+            val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
+            assumeTrue(apiLevel >= min)
         }
 
-        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
         fun assumeBeforeVendorApiLevel(max: Int) {
-            val version = WindowSdkExtensions.getInstance().extensionVersion
-            assumeTrue(version < max)
-            assumeTrue(version > 0)
+            val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
+            assumeTrue(apiLevel < max)
+            assumeTrue(apiLevel > 0)
+        }
+
+        fun isInMultiWindowMode(activity: Activity): Boolean {
+            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                activity.isInMultiWindowMode
+            } else false
+        }
+
+        fun assumePlatformBeforeR() {
+            assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
+        }
+
+        fun assumePlatformROrAbove() {
+            assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+        }
+
+        fun assumePlatformBeforeU() {
+            assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        }
+
+        fun assumePlatformUOrAbove() {
+            assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+        }
+
+        /**
+         * Creates and launches an activity performing the supplied actions at various points in the
+         * activity lifecycle.
+         *
+         * @param initialAction the action that will run once before the activity is created.
+         * @param verifyAction the action to run once after each change in activity lifecycle state.
+         */
+        fun runActionsAcrossActivityLifecycle(
+            scenarioRule: ActivityScenarioRule<TestActivity>,
+            initialAction: ActivityScenario.ActivityAction<TestActivity>,
+            verifyAction: ActivityScenario.ActivityAction<TestActivity>
+        ) {
+            val scenario = scenarioRule.scenario
+            scenario.onActivity(initialAction)
+            scenario.moveToState(Lifecycle.State.CREATED)
+            scenario.onActivity(verifyAction)
+            scenario.moveToState(Lifecycle.State.STARTED)
+            scenario.onActivity(verifyAction)
+            scenario.moveToState(Lifecycle.State.RESUMED)
+            scenario.onActivity(verifyAction)
         }
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
index 3d658dc..76daa1b 100644
--- a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.WindowExtensionsProvider
+import kotlin.test.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assume.assumeTrue
 import org.junit.Test
@@ -35,15 +36,15 @@
      */
     @Test
     fun windowAreaComponentIsAvailable_ifProviderIsAvailable() {
-        assumeTrue(ExtensionsUtil.safeVendorApiLevel >= 2)
+        assumeTrue(ExtensionsUtil.safeVendorApiLevel >= 3)
         val loader = SafeWindowAreaComponentProvider::class.java.classLoader!!
         val safeComponent = SafeWindowAreaComponentProvider(loader).windowAreaComponent
 
         try {
             val extensions = WindowExtensionsProvider.getWindowExtensions()
             val actualComponent = extensions.windowAreaComponent
-            if (actualComponent == null) {
-                assertNull(safeComponent)
+            if (actualComponent != null) {
+                assertNotNull(safeComponent)
             }
             // TODO(b/267831038): verify upon each api level
             // TODO(b/267708462): more reliable test for testing actual method matching
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
index 21ecaa1..514ab50 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -23,6 +23,7 @@
 import android.os.Build
 import android.util.DisplayMetrics
 import android.view.View
+import android.view.Window
 import android.widget.TextView
 import androidx.annotation.RequiresApi
 import androidx.test.ext.junit.rules.ActivityScenarioRule
@@ -84,11 +85,7 @@
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             activityScenario.scenario.onActivity {
                 val extensionComponent = FakeWindowAreaComponent()
-                val controller =
-                    WindowAreaControllerImpl(
-                        windowAreaComponent = extensionComponent,
-                        vendorApiLevel = FEATURE_VENDOR_API_LEVEL
-                    )
+                val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
                 extensionComponent.currentRearDisplayStatus = STATUS_UNAVAILABLE
                 extensionComponent.currentRearDisplayPresentationStatus = STATUS_UNAVAILABLE
                 val collector = TestWindowAreaInfoListConsumer()
@@ -168,11 +165,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(
-                    windowAreaComponent = extensions,
-                    vendorApiLevel = FEATURE_VENDOR_API_LEVEL
-                )
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
             extensions.currentRearDisplayStatus = STATUS_AVAILABLE
             val callback = TestWindowAreaSessionCallback()
             val windowAreaInfo: WindowAreaInfo? =
@@ -243,11 +236,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(
-                    windowAreaComponent = extensions,
-                    vendorApiLevel = FEATURE_VENDOR_API_LEVEL
-                )
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
             extensions.currentRearDisplayStatus = initialState
             val callback = TestWindowAreaSessionCallback()
             val windowAreaInfo: WindowAreaInfo? =
@@ -291,11 +280,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(
-                    windowAreaComponent = extensions,
-                    vendorApiLevel = FEATURE_VENDOR_API_LEVEL
-                )
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
             extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
             extensions.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
@@ -345,8 +330,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
             extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
             extensions.updateRearDisplayPresentationStatusListeners(STATUS_AVAILABLE)
@@ -366,8 +350,7 @@
             }
 
             // Create a new controller to start the presentation.
-            val controller2 =
-                WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+            val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
             val callback = TestWindowAreaPresentationSessionCallback()
             activityScenario.scenario.onActivity { testActivity ->
@@ -401,8 +384,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
             extensions.currentRearDisplayStatus = STATUS_AVAILABLE
             val callback = TestWindowAreaSessionCallback()
             val windowAreaInfo =
@@ -426,8 +408,7 @@
             }
 
             // Create a new controller to start the transfer.
-            val controller2 =
-                WindowAreaControllerImpl(windowAreaComponent = extensions, vendorApiLevel = 3)
+            val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
 
             activityScenario.scenario.onActivity { testActivity ->
                 assert(
@@ -464,11 +445,7 @@
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensionComponent = FakeWindowAreaComponent()
-            val controller =
-                WindowAreaControllerImpl(
-                    windowAreaComponent = extensionComponent,
-                    vendorApiLevel = FEATURE_VENDOR_API_LEVEL
-                )
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
 
             extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
             extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
@@ -580,7 +557,7 @@
             rearDisplayPresentationSessionConsumer?.accept(SESSION_STATE_INACTIVE)
         }
 
-        override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+        override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation {
             return TestExtensionWindowAreaPresentation(
                 testActivity!!,
                 rearDisplayPresentationSessionConsumer!!
@@ -675,11 +652,13 @@
         override fun setPresentationView(view: View) {
             sessionConsumer.accept(WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE)
         }
+
+        override fun getWindow(): Window {
+            return activity.window
+        }
     }
 
     companion object {
         private const val REAR_FACING_BINDER_DESCRIPTION = "TEST_WINDOW_AREA_REAR_FACING"
-
-        private const val FEATURE_VENDOR_API_LEVEL = 3
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
index 2f3a207..a54dc60a 100644
--- a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -37,33 +37,28 @@
         assertTrue(
             WindowAreaComponentValidator.isWindowAreaComponentValid(
                 WindowAreaComponentFullImplementation::class.java,
-                2
-            )
-        )
-        assertTrue(
-            WindowAreaComponentValidator.isWindowAreaComponentValid(
-                WindowAreaComponentFullImplementation::class.java,
-                3
+                apiLevel = 3
             )
         )
     }
 
-    /**
-     * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
-     * implementation.
-     */
     @Test
-    fun isWindowAreaComponentValid_apiLevel2() {
-        assertTrue(
-            WindowAreaComponentValidator.isWindowAreaComponentValid(
-                WindowAreaComponentApiV2Implementation::class.java,
-                2
-            )
-        )
+    fun isWindowAreaComponentValid_apiLevel1() {
         assertFalse(
             WindowAreaComponentValidator.isWindowAreaComponentValid(
-                IncompleteWindowAreaComponentApiV2Implementation::class.java,
-                3
+                WindowAreaComponentApiV3Implementation::class.java,
+                apiLevel = 1
+            )
+        )
+    }
+
+    /** Test that validator returns false for API Level 2. */
+    @Test
+    fun isWindowAreaComponentValid_apiLevel2() {
+        assertFalse(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentApiV3Implementation::class.java,
+                2
             )
         )
     }
@@ -74,7 +69,7 @@
      */
     @Test
     fun isWindowAreaComponentValid_apiLevel3() {
-        assertTrue(
+        assertFalse(
             WindowAreaComponentValidator.isWindowAreaComponentValid(
                 WindowAreaComponentApiV3Implementation::class.java,
                 2
@@ -99,10 +94,14 @@
         )
     }
 
-    /** Test that validator returns true if the [ExtensionWindowAreaStatus] is valid */
+    /**
+     * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid and expected to
+     * be found on that vendorApiLevel. Verifies that true is returned if the vendorApiLevel is a
+     * version before [ExtensionWindowAreaStatus] was introduced.
+     */
     @Test
     fun isExtensionWindowAreaStatusValid_trueIfValid() {
-        assertTrue(
+        assertFalse(
             WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
                 ValidExtensionWindowAreaStatus::class.java,
                 2
@@ -116,18 +115,15 @@
         )
     }
 
-    /** Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete */
+    /**
+     * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete and
+     * expected to be on the device.
+     */
     @Test
     fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
         assertFalse(
             WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
                 IncompleteExtensionWindowAreaStatus::class.java,
-                2
-            )
-        )
-        assertFalse(
-            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
-                IncompleteExtensionWindowAreaStatus::class.java,
                 3
             )
         )
@@ -177,24 +173,6 @@
         }
     }
 
-    private class WindowAreaComponentApiV2Implementation : WindowAreaComponentApi2Requirements {
-        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
-            throw NotImplementedError("Not implemented")
-        }
-
-        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
-            throw NotImplementedError("Not implemented")
-        }
-
-        override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
-            throw NotImplementedError("Not implemented")
-        }
-
-        override fun endRearDisplaySession() {
-            throw NotImplementedError("Not implemented")
-        }
-    }
-
     private class WindowAreaComponentApiV3Implementation : WindowAreaComponentApi3Requirements {
         override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
             throw NotImplementedError("Not implemented")
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
index fc53f9e..6e8a2a4 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -17,20 +17,28 @@
 package androidx.window.embedding
 
 import android.app.Activity
+import android.content.res.Resources
+import android.graphics.Color
 import android.os.Binder
 import android.os.IBinder
 import androidx.window.WindowSdkExtensions
 import androidx.window.WindowTestUtils
 import androidx.window.core.PredicateAdapter
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
 import androidx.window.embedding.SplitAttributes.SplitType
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
 import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.ActivityStack.Token as OEMActivityStackToken
+import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
+import androidx.window.extensions.embedding.AnimationParams as OEMEmbeddingAnimationParams
+import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
 import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
 import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
 import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import androidx.window.extensions.embedding.SplitInfo.Token as OEMSplitInfoToken
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -39,6 +47,7 @@
 
 /** Tests for [EmbeddingAdapter] */
 class EmbeddingAdapterTest {
+
     private lateinit var adapter: EmbeddingAdapter
 
     private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
@@ -52,51 +61,158 @@
     }
 
     @Test
-    fun testTranslateSplitInfoWithDefaultAttrs() {
+    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel2() {
         WindowTestUtils.assumeAtLeastVendorApiLevel(2)
         WindowTestUtils.assumeBeforeVendorApiLevel(3)
 
-        val oemSplitInfo =
-            createTestOEMSplitInfo(
-                createTestOEMActivityStack(ArrayList(), true),
-                createTestOEMActivityStack(ArrayList(), true),
-                OEMSplitAttributes.Builder().build(),
-            )
+        val oemSplitInfo = createTestOEMSplitInfo(OEMSplitAttributes.Builder().build())
         val expectedSplitInfo =
             SplitInfo(
-                ActivityStack(ArrayList(), isEmpty = true),
-                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
-                INVALID_SPLIT_INFO_TOKEN,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel3() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                OEMSplitAttributes.Builder().build(),
+                testBinder = Binder(),
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                SplitAttributes.Builder()
+                    .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    .build(),
+                oemSplitInfo.token,
             )
         assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
     }
 
     @Test
-    fun testTranslateSplitInfoWithExpandingContainers() {
+    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel5() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                OEMSplitAttributes.Builder().build(),
+                testToken = OEMSplitInfoToken.createFromBinder(Binder()),
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(
+                    ArrayList(),
+                    isEmpty = true,
+                    oemSplitInfo.primaryActivityStack.activityStackToken,
+                ),
+                ActivityStack(
+                    ArrayList(),
+                    isEmpty = true,
+                    oemSplitInfo.secondaryActivityStack.activityStackToken,
+                ),
+                SplitAttributes.Builder()
+                    .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    .build(),
+                oemSplitInfo.splitInfoToken,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel2() {
         WindowTestUtils.assumeAtLeastVendorApiLevel(2)
         WindowTestUtils.assumeBeforeVendorApiLevel(3)
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
-                createTestOEMActivityStack(ArrayList(), true),
-                createTestOEMActivityStack(ArrayList(), true),
                 OEMSplitAttributes.Builder()
                     .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
-                    .build(),
+                    .build()
             )
         val expectedSplitInfo =
             SplitInfo(
-                ActivityStack(ArrayList(), isEmpty = true),
-                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
-                INVALID_SPLIT_INFO_TOKEN,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel3() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                OEMSplitAttributes.Builder()
+                    .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
+                    .build(),
+                testBinder = Binder(),
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                SplitAttributes.Builder()
+                    .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    .build(),
+                oemSplitInfo.token,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel5() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                OEMSplitAttributes.Builder()
+                    .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
+                    .build(),
+                testToken = OEMSplitInfoToken.createFromBinder(Binder()),
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(
+                    ArrayList(),
+                    isEmpty = true,
+                    oemSplitInfo.primaryActivityStack.activityStackToken
+                ),
+                ActivityStack(
+                    ArrayList(),
+                    isEmpty = true,
+                    oemSplitInfo.secondaryActivityStack.activityStackToken
+                ),
+                SplitAttributes.Builder()
+                    .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    .build(),
+                oemSplitInfo.splitInfoToken,
             )
         assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
     }
@@ -117,14 +233,13 @@
 
         val expectedSplitInfo =
             SplitInfo(
-                ActivityStack(ArrayList(), isEmpty = true),
-                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.ratio(expectedSplitRatio))
                     // OEMSplitInfo with Vendor API level 1 doesn't provide layoutDirection.
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
-                INVALID_SPLIT_INFO_TOKEN,
             )
         assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
     }
@@ -136,8 +251,6 @@
 
         val oemSplitInfo =
             createTestOEMSplitInfo(
-                createTestOEMActivityStack(ArrayList(), true),
-                createTestOEMActivityStack(ArrayList(), true),
                 OEMSplitAttributes.Builder()
                     .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
                     .setLayoutDirection(TOP_TO_BOTTOM)
@@ -145,53 +258,425 @@
             )
         val expectedSplitInfo =
             SplitInfo(
-                ActivityStack(ArrayList(), isEmpty = true),
-                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SPLIT_TYPE_HINGE)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
                     .build(),
-                INVALID_SPLIT_INFO_TOKEN,
-            )
-        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
-    }
-
-    @Test
-    fun testTranslateSplitInfoWithApiLevel3() {
-        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
-        WindowTestUtils.assumeBeforeVendorApiLevel(5)
-
-        val testStackToken = Binder()
-        val testSplitInfoToken = Binder()
-        val oemSplitInfo =
-            createTestOEMSplitInfo(
-                createTestOEMActivityStack(ArrayList(), true, testStackToken),
-                createTestOEMActivityStack(ArrayList(), true, testStackToken),
-                OEMSplitAttributes.Builder()
-                    .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
-                    .setLayoutDirection(TOP_TO_BOTTOM)
-                    .build(),
-                testSplitInfoToken,
-            )
-        val expectedSplitInfo =
-            SplitInfo(
-                ActivityStack(ArrayList(), isEmpty = true),
-                ActivityStack(ArrayList(), isEmpty = true),
-                SplitAttributes.Builder()
-                    .setSplitType(SPLIT_TYPE_HINGE)
-                    .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
-                    .build(),
-                testSplitInfoToken,
             )
         assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
     }
 
     @Suppress("DEPRECATION")
+    @Test
+    fun testTranslateSplitInfoWithApiLevel3() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
+        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                OEMSplitAttributes.Builder()
+                    .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
+                    .setLayoutDirection(TOP_TO_BOTTOM)
+                    .build(),
+                testBinder = Binder()
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(ArrayList(), isEmpty = true),
+                ActivityStack(ArrayList(), isEmpty = true),
+                SplitAttributes.Builder()
+                    .setSplitType(SPLIT_TYPE_HINGE)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+                    .build(),
+                oemSplitInfo.token,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithApiLevel5() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+        val oemSplitInfo =
+            createTestOEMSplitInfo(
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
+                OEMSplitAttributes.Builder()
+                    .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
+                    .setLayoutDirection(TOP_TO_BOTTOM)
+                    .build(),
+                testToken = OEMSplitInfoToken.createFromBinder(Binder())
+            )
+        val expectedSplitInfo =
+            SplitInfo(
+                ActivityStack(
+                    emptyList(),
+                    isEmpty = true,
+                    oemSplitInfo.primaryActivityStack.activityStackToken,
+                ),
+                ActivityStack(
+                    emptyList(),
+                    isEmpty = true,
+                    oemSplitInfo.secondaryActivityStack.activityStackToken,
+                ),
+                SplitAttributes.Builder()
+                    .setSplitType(SPLIT_TYPE_HINGE)
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+                    .build(),
+                token = oemSplitInfo.splitInfoToken,
+            )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateAnimationBackgroundWithApiLevel7() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+
+        val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+        val animationParamsWithColorBackground =
+            EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+        val splitAttributesWithColorBackground =
+            SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+        val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+        val splitAttributesWithDefaultBackground =
+            SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+        val extensionsColorBackground =
+            OEMEmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+        val extensionAnimationParamsWithColorBackground =
+            OEMEmbeddingAnimationParams.Builder()
+                .setAnimationBackground(extensionsColorBackground)
+                .build()
+        val extensionsSplitAttributesWithColorBackground =
+            OEMSplitAttributes.Builder()
+                .setAnimationParams(extensionAnimationParamsWithColorBackground)
+                .build()
+
+        val extensionAnimationParamsWithDefaultBackground =
+            OEMEmbeddingAnimationParams.Builder()
+                .setAnimationBackground(
+                    OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+                )
+                .build()
+        val extensionsSplitAttributesWithDefaultBackground =
+            OEMSplitAttributes.Builder()
+                .setAnimationParams(extensionAnimationParamsWithDefaultBackground)
+                .build()
+
+        // Translate from Window to Extensions
+        assertEquals(
+            extensionsSplitAttributesWithColorBackground,
+            adapter.translateSplitAttributes(splitAttributesWithColorBackground)
+        )
+        assertEquals(
+            extensionsSplitAttributesWithDefaultBackground,
+            adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+        )
+
+        // Translate from Extensions to Window
+        assertEquals(
+            splitAttributesWithColorBackground,
+            adapter.translate(extensionsSplitAttributesWithColorBackground)
+        )
+        assertEquals(
+            splitAttributesWithDefaultBackground,
+            adapter.translate(extensionsSplitAttributesWithDefaultBackground)
+        )
+    }
+
+    @Test
+    @Suppress("DEPRECATION")
+    fun testTranslateAnimationBackgroundWithApiLevel5And6() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+        WindowTestUtils.assumeBeforeVendorApiLevel(7)
+
+        val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+        val animationParamsWithColorBackground =
+            EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+        val splitAttributesWithColorBackground =
+            SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+        val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+        val splitAttributesWithDefaultBackground =
+            SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+        val extensionsColorBackground =
+            OEMEmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+        val extensionsSplitAttributesWithColorBackground =
+            OEMSplitAttributes.Builder().setAnimationBackground(extensionsColorBackground).build()
+        val extensionsSplitAttributesWithDefaultBackground =
+            OEMSplitAttributes.Builder()
+                .setAnimationBackground(
+                    OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+                )
+                .build()
+
+        // Translate from Window to Extensions
+        assertEquals(
+            extensionsSplitAttributesWithColorBackground,
+            adapter.translateSplitAttributes(splitAttributesWithColorBackground)
+        )
+        assertEquals(
+            extensionsSplitAttributesWithDefaultBackground,
+            adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+        )
+
+        // Translate from Extensions to Window
+        assertEquals(
+            splitAttributesWithColorBackground,
+            adapter.translate(extensionsSplitAttributesWithColorBackground)
+        )
+        assertEquals(
+            splitAttributesWithDefaultBackground,
+            adapter.translate(extensionsSplitAttributesWithDefaultBackground)
+        )
+    }
+
+    @Test
+    @Suppress("DEPRECATION")
+    fun testTranslateAnimationBackgroundBeforeApiLevel5() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
+        WindowTestUtils.assumeBeforeVendorApiLevel(5)
+
+        val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+        val animationParamsWithColorBackground =
+            EmbeddingAnimationParams.Builder().setAnimationBackground(colorBackground).build()
+        val splitAttributesWithColorBackground =
+            SplitAttributes.Builder().setAnimationParams(animationParamsWithColorBackground).build()
+        val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+        val splitAttributesWithDefaultBackground =
+            SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+        // No difference after translate before API level 5
+        assertEquals(
+            adapter.translateSplitAttributes(splitAttributesWithColorBackground),
+            adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
+        )
+    }
+
+    @Test
+    fun testTranslateAnimationSpecWithApiLevel7() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+
+        val animationParamsWithJumpCut =
+            EmbeddingAnimationParams.Builder()
+                .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .build()
+        val splitAttributesWithJumpCutAnimationParams =
+            SplitAttributes.Builder().setAnimationParams(animationParamsWithJumpCut).build()
+        val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+        val splitAttributesWithDefaultAnimationParams =
+            SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+        val extensionsAnimationParamsWithJumpCut =
+            OEMEmbeddingAnimationParams.Builder()
+                .setOpenAnimationResId(Resources.ID_NULL)
+                .setCloseAnimationResId(Resources.ID_NULL)
+                .setChangeAnimationResId(Resources.ID_NULL)
+                .build()
+        val extensionsSplitAttributesWithJumpCutAnimationParams =
+            OEMSplitAttributes.Builder()
+                .setAnimationParams(extensionsAnimationParamsWithJumpCut)
+                .build()
+        val oemDefaultAnimationParams = OEMEmbeddingAnimationParams.Builder().build()
+        val extensionsSplitAttributesWithDefaultAnimationParams =
+            OEMSplitAttributes.Builder().setAnimationParams(oemDefaultAnimationParams).build()
+
+        // Translate from Window to Extensions
+        assertEquals(
+            extensionsSplitAttributesWithJumpCutAnimationParams,
+            adapter.translateSplitAttributes(splitAttributesWithJumpCutAnimationParams)
+        )
+        assertEquals(
+            extensionsSplitAttributesWithDefaultAnimationParams,
+            adapter.translateSplitAttributes(splitAttributesWithDefaultAnimationParams)
+        )
+
+        // Translate from Extensions to Window
+        assertEquals(
+            splitAttributesWithJumpCutAnimationParams,
+            adapter.translate(extensionsSplitAttributesWithJumpCutAnimationParams)
+        )
+        assertEquals(
+            splitAttributesWithDefaultAnimationParams,
+            adapter.translate(extensionsSplitAttributesWithDefaultAnimationParams)
+        )
+    }
+
+    @Test
+    fun testTranslateAnimationSpecBeforeApiLevel7() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
+        WindowTestUtils.assumeBeforeVendorApiLevel(7)
+
+        val animationParamsWithJumpCut =
+            EmbeddingAnimationParams.Builder()
+                .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .build()
+        val splitAttributesWithJumpCutAnimationParams =
+            SplitAttributes.Builder().setAnimationParams(animationParamsWithJumpCut).build()
+        val defaultAnimationParams = EmbeddingAnimationParams.Builder().build()
+        val splitAttributesWithDefaultAnimationParams =
+            SplitAttributes.Builder().setAnimationParams(defaultAnimationParams).build()
+
+        // No difference after translate before API level 7
+        assertEquals(
+            adapter.translateSplitAttributes(splitAttributesWithJumpCutAnimationParams),
+            adapter.translateSplitAttributes(splitAttributesWithDefaultAnimationParams)
+        )
+    }
+
+    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+    @Test
+    fun testTranslateEmbeddingConfigurationToWindowAttributes() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
+
+        val dimAreaBehavior = EmbeddingConfiguration.DimAreaBehavior.ON_TASK
+        adapter.embeddingConfiguration = EmbeddingConfiguration(dimAreaBehavior)
+        val oemSplitAttributes = adapter.translateSplitAttributes(SplitAttributes.Builder().build())
+
+        assertEquals(dimAreaBehavior.value, oemSplitAttributes.windowAttributes.dimAreaBehavior)
+    }
+
+    @Test
+    fun testTranslateDividerAttributes_draggable() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        val dividerAttributes =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(20)
+                .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+                .setColor(Color.GRAY)
+                .build()
+        val oemDividerAttributes =
+            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                .setWidthDp(20)
+                .setPrimaryMinRatio(0.3f)
+                .setPrimaryMaxRatio(0.7f)
+                .setDividerColor(Color.GRAY)
+                .build()
+
+        assertEquals(
+            oemDividerAttributes,
+            adapter.translateToOemDividerAttributes(dividerAttributes)
+        )
+        assertEquals(
+            dividerAttributes,
+            adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+        )
+    }
+
+    @Test
+    fun testTranslateDividerAttributes_dragToFullscreen() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(7)
+        val dividerAttributes =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(20)
+                .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+                .setColor(Color.GRAY)
+                .setDraggingToFullscreenAllowed(true)
+                .build()
+        val oemDividerAttributes =
+            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                .setWidthDp(20)
+                .setPrimaryMinRatio(0.3f)
+                .setPrimaryMaxRatio(0.7f)
+                .setDividerColor(Color.GRAY)
+                .setDraggingToFullscreenAllowed(true)
+                .build()
+
+        val dividerAttributes2 =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(20)
+                .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+                .setColor(Color.GRAY)
+                .build()
+        val oemDividerAttributes2 =
+            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                .setWidthDp(20)
+                .setPrimaryMinRatio(0.3f)
+                .setPrimaryMaxRatio(0.7f)
+                .setDividerColor(Color.GRAY)
+                .build()
+
+        assertEquals(
+            oemDividerAttributes,
+            adapter.translateToOemDividerAttributes(dividerAttributes)
+        )
+        assertEquals(
+            dividerAttributes,
+            adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+        )
+        assertEquals(
+            oemDividerAttributes2,
+            adapter.translateToOemDividerAttributes(dividerAttributes2)
+        )
+        assertEquals(
+            dividerAttributes2,
+            adapter.translateToJetpackDividerAttributes(oemDividerAttributes2)
+        )
+    }
+
+    @Test
+    fun testTranslateDividerAttributes_fixed() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        val dividerAttributes =
+            FixedDividerAttributes.Builder().setWidthDp(20).setColor(Color.GRAY).build()
+        val oemDividerAttributes =
+            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_FIXED)
+                .setWidthDp(20)
+                .setDividerColor(Color.GRAY)
+                .build()
+
+        assertEquals(
+            oemDividerAttributes,
+            adapter.translateToOemDividerAttributes(dividerAttributes)
+        )
+        assertEquals(
+            dividerAttributes,
+            adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+        )
+    }
+
+    @Test
+    fun testTranslateDividerAttributes_noDivider() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
+        val dividerAttributes = DividerAttributes.NO_DIVIDER
+        val oemDividerAttributes = null
+
+        assertEquals(
+            oemDividerAttributes,
+            adapter.translateToOemDividerAttributes(dividerAttributes)
+        )
+        assertEquals(
+            dividerAttributes,
+            adapter.translateToJetpackDividerAttributes(oemDividerAttributes)
+        )
+    }
+
+    private fun createTestOEMSplitInfo(
+        testSplitAttributes: OEMSplitAttributes,
+        testBinder: IBinder? = null,
+        testToken: OEMSplitInfo.Token? = null,
+    ): OEMSplitInfo =
+        createTestOEMSplitInfo(
+            createTestOEMActivityStack(),
+            createTestOEMActivityStack(),
+            testSplitAttributes,
+            testBinder,
+            testToken,
+        )
+
+    @Suppress("Deprecation") // Verify the behavior of version 3 and 4.
     private fun createTestOEMSplitInfo(
         testPrimaryActivityStack: OEMActivityStack,
         testSecondaryActivityStack: OEMActivityStack,
         testSplitAttributes: OEMSplitAttributes,
-        testToken: IBinder = INVALID_SPLIT_INFO_TOKEN,
+        testBinder: IBinder? = null,
+        testToken: OEMSplitInfoToken? = null,
     ): OEMSplitInfo {
         return mock<OEMSplitInfo>().apply {
             whenever(primaryActivityStack).thenReturn(testPrimaryActivityStack)
@@ -199,23 +684,32 @@
             if (extensionVersion >= 2) {
                 whenever(splitAttributes).thenReturn(testSplitAttributes)
             }
-            if (extensionVersion >= 3) {
-                whenever(token).thenReturn(testToken)
+            when (extensionVersion) {
+                in 3..4 -> whenever(token).thenReturn(testBinder)
+                in 5..Int.MAX_VALUE -> whenever(splitInfoToken).thenReturn(testToken)
             }
         }
     }
 
-    @Suppress("DEPRECATION")
+    private fun createTestOEMActivityStack(
+        testToken: OEMActivityStackToken? = null
+    ): OEMActivityStack =
+        createTestOEMActivityStack(
+            emptyList(),
+            testIsEmpty = true,
+            testToken,
+        )
+
     private fun createTestOEMActivityStack(
         testActivities: List<Activity>,
         testIsEmpty: Boolean,
-        testToken: IBinder = INVALID_ACTIVITY_STACK_TOKEN,
+        testToken: OEMActivityStackToken? = null,
     ): OEMActivityStack {
         return mock<OEMActivityStack>().apply {
             whenever(activities).thenReturn(testActivities)
             whenever(isEmpty).thenReturn(testIsEmpty)
-            if (extensionVersion in 3..4) {
-                whenever(token).thenReturn(testToken)
+            if (extensionVersion >= 5) {
+                whenever(activityStackToken).thenReturn(testToken)
             }
         }
     }
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
index 7fa2cc6..796862e 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
 import android.graphics.Rect
 import android.os.Build
 import androidx.annotation.RequiresApi
@@ -79,6 +80,7 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         assertNull(rule.tag)
         assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -136,6 +138,16 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(TOP_TO_BOTTOM)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
+                        )
+                        .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         assertEquals(TEST_TAG, rule.tag)
         assertEquals(NEVER, rule.finishPrimaryWithSecondary)
@@ -163,6 +175,7 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         assertNull(rule.tag)
         assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -226,6 +239,18 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(BOTTOM_TO_TOP)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(
+                                application.resources.getColor(R.color.testColor, null)
+                            )
+                        )
+                        .setOpenAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .setChangeAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         assertEquals(TEST_TAG, rule.tag)
         assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
index 447640c..be687ee 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
@@ -17,8 +17,8 @@
 package androidx.window.embedding
 
 import android.util.Log
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.WindowExtensions
 import androidx.window.extensions.WindowExtensionsProvider
 import org.junit.Assert.assertNotNull
@@ -63,9 +63,13 @@
                 // TODO(b/267708462) : more reliable test for testing actual method matching
                 assertNotNull(safeComponent)
                 assertTrue(safeProvider.isActivityEmbeddingComponentAccessible())
-                when (ExtensionsUtil.safeVendorApiLevel) {
+                when (WindowSdkExtensions.getInstance().extensionVersion) {
                     1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
-                    else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+                    2 -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+                    in 3..4 -> assertTrue(safeProvider.hasValidVendorApiLevel3())
+                    5 -> assertTrue(safeProvider.hasValidVendorApiLevel5())
+                    6 -> assertTrue(safeProvider.hasValidVendorApiLevel6())
+                    7 -> assertTrue(safeProvider.hasValidVendorApiLevel7())
                 }
             }
         } catch (e: UnsupportedOperationException) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
index 784a5c5..1c166e87 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
@@ -52,9 +52,14 @@
                 // TODO(b/267708462): more reliable test for testing actual method matching
                 assertNotNull(safeComponent)
                 assertTrue(safeProvider.isWindowLayoutComponentAccessible())
-                when (ExtensionsUtil.safeVendorApiLevel) {
-                    1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
-                    else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
+                when {
+                    ExtensionsUtil.safeVendorApiLevel == 1 -> {
+                        assertTrue(safeProvider.hasValidVendorApiLevel1())
+                    }
+                    ExtensionsUtil.safeVendorApiLevel < 6 -> {
+                        assertTrue(safeProvider.hasValidVendorApiLevel2())
+                    }
+                    else -> assertTrue(safeProvider.hasValidVendorApiLevel6())
                 }
             }
         } catch (e: UnsupportedOperationException) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
index e1a57bc..4560d2f 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
@@ -22,10 +22,13 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.TestActivity
 import androidx.window.TestConsumer
+import androidx.window.WindowSdkExtensions
 import androidx.window.WindowTestUtils
 import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
 import androidx.window.layout.adapter.WindowBackend
 import java.util.concurrent.Executor
+import kotlin.test.assertEquals
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -34,6 +37,7 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.test.setMain
+import org.junit.Assert.assertThrows
 import org.junit.Rule
 import org.junit.Test
 
@@ -45,6 +49,7 @@
         ActivityScenarioRule(TestActivity::class.java)
 
     private val testScope = TestScope(UnconfinedTestDispatcher())
+    private val windowSdkExtensions = WindowSdkExtensions.getInstance()
 
     init {
         Dispatchers.setMain(UnconfinedTestDispatcher())
@@ -54,9 +59,10 @@
     public fun testWindowLayoutFeatures(): Unit =
         testScope.runTest {
             activityScenario.scenario.onActivity { testActivity ->
-                val windowMetricsCalculator = WindowMetricsCalculatorCompat
+                val windowMetricsCalculator = WindowMetricsCalculatorCompat()
                 val fakeBackend = FakeWindowBackend()
-                val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+                val repo =
+                    WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
                 val collector = TestConsumer<WindowLayoutInfo>()
                 testScope.launch(Job()) {
                     repo.windowLayoutInfo(testActivity).collect(collector::accept)
@@ -74,7 +80,12 @@
             }
             assumeAtLeastVendorApiLevel(2)
             val fakeBackend = FakeWindowBackend()
-            val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, fakeBackend)
+            val repo =
+                WindowInfoTrackerImpl(
+                    WindowMetricsCalculatorCompat(),
+                    fakeBackend,
+                    windowSdkExtensions
+                )
             val collector = TestConsumer<WindowLayoutInfo>()
 
             val windowContext = WindowTestUtils.createOverlayWindowContext()
@@ -89,9 +100,10 @@
     public fun testWindowLayoutFeatures_multicasting(): Unit =
         testScope.runTest {
             activityScenario.scenario.onActivity { testActivity ->
-                val windowMetricsCalculator = WindowMetricsCalculatorCompat
+                val windowMetricsCalculator = WindowMetricsCalculatorCompat()
                 val fakeBackend = FakeWindowBackend()
-                val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+                val repo =
+                    WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
                 val collector = TestConsumer<WindowLayoutInfo>()
                 val job = Job()
                 launch(job) { repo.windowLayoutInfo(testActivity).collect(collector::accept) }
@@ -102,15 +114,43 @@
         }
 
     @Test
+    fun testSupportedWindowPostures_throwsBeforeApi6() {
+        assumeBeforeVendorApiLevel(6)
+        activityScenario.scenario.onActivity { _ ->
+            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
+            val fakeBackend = FakeWindowBackend()
+            val repo =
+                WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
+            assertThrows(UnsupportedOperationException::class.java) { repo.supportedPostures }
+        }
+    }
+
+    @Test
+    fun testSupportedWindowPostures_reportsFeatures() {
+        assumeAtLeastVendorApiLevel(6)
+        activityScenario.scenario.onActivity { _ ->
+            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
+            val expected = listOf(SupportedPosture.TABLETOP)
+            val fakeBackend = FakeWindowBackend(supportedPostures = expected)
+            val repo =
+                WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
+            val actual = repo.supportedPostures
+
+            assertEquals(expected, actual)
+        }
+    }
+
+    @Test
     public fun testWindowLayoutFeatures_multicastingWithContext(): Unit =
         testScope.runTest {
             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
                 return@runTest
             }
             assumeAtLeastVendorApiLevel(2)
-            val windowMetricsCalculator = WindowMetricsCalculatorCompat
+            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
             val fakeBackend = FakeWindowBackend()
-            val repo = WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend)
+            val repo =
+                WindowInfoTrackerImpl(windowMetricsCalculator, fakeBackend, windowSdkExtensions)
             val collector = TestConsumer<WindowLayoutInfo>()
             val job = Job()
 
@@ -123,7 +163,9 @@
             collector.assertValues(WindowLayoutInfo(emptyList()), WindowLayoutInfo(emptyList()))
         }
 
-    private class FakeWindowBackend : WindowBackend {
+    private class FakeWindowBackend(
+        override val supportedPostures: List<SupportedPosture> = emptyList()
+    ) : WindowBackend {
 
         private class CallbackHolder(
             val executor: Executor,
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
index 16caeae..4096e1e 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
@@ -16,19 +16,24 @@
 package androidx.window.layout
 
 import android.annotation.SuppressLint
-import android.app.Activity
 import android.content.ContextWrapper
 import android.os.Build
 import android.view.Display
 import android.view.WindowManager
+import androidx.annotation.RequiresApi
 import androidx.core.view.WindowInsetsCompat
-import androidx.lifecycle.Lifecycle
 import androidx.test.core.app.ActivityScenario.ActivityAction
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeR
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
+import androidx.window.WindowTestUtils.Companion.isInMultiWindowMode
+import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
 import androidx.window.core.ExperimentalWindowApi
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
 import org.junit.Assume
@@ -124,8 +129,9 @@
     @Test
     fun testGetCurrentWindowBounds_postR() {
         assumePlatformROrAbove()
-        runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
-            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val bounds =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
             val windowMetricsBounds = activity.windowManager.currentWindowMetrics.bounds
             assertEquals(windowMetricsBounds, bounds)
         }
@@ -199,8 +205,9 @@
     @Test
     fun testGetMaximumWindowBounds_postR() {
         assumePlatformROrAbove()
-        runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
-            val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val bounds =
+                WindowMetricsCalculatorCompat().computeMaximumWindowMetrics(activity).bounds
             val windowMetricsBounds = activity.windowManager.maximumWindowMetrics.bounds
             assertEquals(windowMetricsBounds, bounds)
         }
@@ -211,8 +218,9 @@
     @OptIn(ExperimentalWindowApi::class)
     fun testGetWindowInsetsCompat_currentWindowMetrics_postR() {
         assumePlatformROrAbove()
-        runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
-            val windowMetrics = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity)
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
             val windowInsets = windowMetrics.getWindowInsets()
             val platformInsets = activity.windowManager.currentWindowMetrics.windowInsets
             val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
@@ -225,8 +233,9 @@
     @OptIn(ExperimentalWindowApi::class)
     fun testGetWindowInsetsCompat_maximumWindowMetrics_postR() {
         assumePlatformROrAbove()
-        runActionsAcrossActivityLifecycle({}) { activity: TestActivity ->
-            val windowMetrics = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity)
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeMaximumWindowMetrics(activity)
             val windowInsets = windowMetrics.getWindowInsets()
             val platformInsets = activity.windowManager.maximumWindowMetrics.windowInsets
             val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
@@ -234,12 +243,41 @@
         }
     }
 
+    @Test
+    fun testDensityMatchesDisplayMetricsDensity() {
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val calculator = WindowMetricsCalculatorCompat()
+            val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
+            val maxWindowMetrics = calculator.computeMaximumWindowMetrics(activity)
+            assertEquals(activity.resources.displayMetrics.density, windowMetrics.density)
+            assertEquals(windowMetrics.density, maxWindowMetrics.density)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testConvertedWindowMetricsMatchesPlatformWindowMetrics() {
+        assumePlatformUOrAbove()
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val calculator = WindowMetricsCalculatorCompat()
+            val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
+            val wm = activity.getSystemService(WindowManager::class.java)
+            val androidWindowMetrics = wm.currentWindowMetrics
+            assertEquals(androidWindowMetrics.bounds, windowMetrics.bounds)
+            assertEquals(androidWindowMetrics.density, windowMetrics.density)
+        }
+    }
+
     private fun testGetCurrentWindowBoundsMatchesRealDisplaySize(
         initialAction: ActivityAction<TestActivity>
     ) {
         val assertWindowBoundsMatchesDisplayAction: ActivityAction<TestActivity> =
             AssertCurrentWindowBoundsEqualsRealDisplaySizeAction()
-        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
+        runActionsAcrossActivityLifecycle(
+            activityScenarioRule,
+            initialAction,
+            assertWindowBoundsMatchesDisplayAction
+        )
     }
 
     private fun testGetMaximumWindowBoundsMatchesRealDisplaySize(
@@ -247,28 +285,11 @@
     ) {
         val assertWindowBoundsMatchesDisplayAction: ActivityAction<TestActivity> =
             AssertMaximumWindowBoundsEqualsRealDisplaySizeAction()
-        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
-    }
-
-    /**
-     * Creates and launches an activity performing the supplied actions at various points in the
-     * activity lifecycle.
-     *
-     * @param initialAction the action that will run once before the activity is created.
-     * @param verifyAction the action to run once after each change in activity lifecycle state.
-     */
-    private fun runActionsAcrossActivityLifecycle(
-        initialAction: ActivityAction<TestActivity>,
-        verifyAction: ActivityAction<TestActivity>
-    ) {
-        val scenario = activityScenarioRule.scenario
-        scenario.onActivity(initialAction)
-        scenario.moveToState(Lifecycle.State.CREATED)
-        scenario.onActivity(verifyAction)
-        scenario.moveToState(Lifecycle.State.STARTED)
-        scenario.onActivity(verifyAction)
-        scenario.moveToState(Lifecycle.State.RESUMED)
-        scenario.onActivity(verifyAction)
+        runActionsAcrossActivityLifecycle(
+            activityScenarioRule,
+            initialAction,
+            assertWindowBoundsMatchesDisplayAction
+        )
     }
 
     private fun assumeNotMultiWindow() {
@@ -295,8 +316,9 @@
                 } else {
                     @Suppress("DEPRECATION") activity.windowManager.defaultDisplay
                 }
-            val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
-            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+            val calculator = WindowMetricsCalculatorCompat()
+            val realDisplaySize = getRealSizeForDisplay(display)
+            val bounds = calculator.computeCurrentWindowMetrics(activity).bounds
             assertNotEquals("Device can not have zero width", 0, realDisplaySize.x.toLong())
             assertNotEquals("Device can not have zero height", 0, realDisplaySize.y.toLong())
             assertEquals(
@@ -321,8 +343,9 @@
                 } else {
                     @Suppress("DEPRECATION") activity.windowManager.defaultDisplay
                 }
-            val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
-            val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
+            val calculator = WindowMetricsCalculatorCompat()
+            val realDisplaySize = getRealSizeForDisplay(display)
+            val bounds = calculator.computeMaximumWindowMetrics(activity).bounds
             assertEquals(
                 "Window bounds width does not match real display width",
                 realDisplaySize.x.toLong(),
@@ -335,20 +358,4 @@
             )
         }
     }
-
-    private companion object {
-        private fun isInMultiWindowMode(activity: Activity): Boolean {
-            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                activity.isInMultiWindowMode
-            } else false
-        }
-
-        private fun assumePlatformBeforeR() {
-            Assume.assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
-        }
-
-        private fun assumePlatformROrAbove() {
-            Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
-        }
-    }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
index fbad2f9..3add8d9 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
@@ -34,81 +34,89 @@
 @RunWith(AndroidJUnit4::class)
 public class WindowMetricsTest {
     @Test
-    public fun testGetBounds() {
+    fun testGetBounds() {
         val bounds = Rect(1, 2, 3, 4)
-        val windowMetrics = WindowMetrics(bounds)
+        val windowMetrics = WindowMetrics(bounds, density = 1f)
         assertEquals(bounds, windowMetrics.bounds)
     }
 
     @Test
-    public fun testEquals_sameBounds() {
+    fun testEquals_sameBounds() {
         val bounds = Rect(1, 2, 3, 4)
-        val windowMetrics0 = WindowMetrics(bounds)
-        val windowMetrics1 = WindowMetrics(bounds)
+        val windowMetrics0 = WindowMetrics(bounds, density = 1f)
+        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
         assertEquals(windowMetrics0, windowMetrics1)
     }
 
     @Test
-    public fun testEquals_differentBounds() {
+    fun testEquals_differentBounds() {
         val bounds0 = Rect(1, 2, 3, 4)
-        val windowMetrics0 = WindowMetrics(bounds0)
+        val windowMetrics0 = WindowMetrics(bounds0, density = 1f)
         val bounds1 = Rect(6, 7, 8, 9)
-        val windowMetrics1 = WindowMetrics(bounds1)
+        val windowMetrics1 = WindowMetrics(bounds1, density = 1f)
         assertNotEquals(windowMetrics0, windowMetrics1)
     }
 
     @Test
-    public fun testHashCode_matchesIfEqual() {
+    fun testEquals_differentDensities() {
         val bounds = Rect(1, 2, 3, 4)
-        val windowMetrics0 = WindowMetrics(bounds)
-        val windowMetrics1 = WindowMetrics(bounds)
+        val windowMetrics0 = WindowMetrics(bounds, density = 0f)
+        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
+        assertNotEquals(windowMetrics0, windowMetrics1)
+    }
+
+    @Test
+    fun testHashCode_matchesIfEqual() {
+        val bounds = Rect(1, 2, 3, 4)
+        val windowMetrics0 = WindowMetrics(bounds, density = 1f)
+        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
         assertEquals(windowMetrics0.hashCode().toLong(), windowMetrics1.hashCode().toLong())
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
     @Test
-    public fun testSameWindowInsets_emptyInsets() {
+    fun testSameWindowInsets_emptyInsets() {
         assumePlatformROrAbove()
         val bounds = Bounds(1, 2, 3, 4)
         val windowInsetsCompat = WindowInsetsCompat.Builder().build()
-        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
-        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
+        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
+        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
         assertEquals(windowMetricsA, windowMetricsB)
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
     @Test
-    public fun testSameWindowInsets_nonEmptyInsets() {
+    fun testSameWindowInsets_nonEmptyInsets() {
         assumePlatformROrAbove()
         val bounds = Bounds(1, 2, 3, 4)
         val insets = Insets.of(6, 7, 8, 9)
         val builder = WindowInsetsCompat.Builder()
-        for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
+        for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
             builder.setInsets(type, insets)
         }
         val windowInsetsCompat = builder.build()
-        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
-        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
+        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
+        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
         assertEquals(windowMetricsA, windowMetricsB)
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
     @Test
-    public fun testDiffWindowInsets() {
+    fun testDiffWindowInsets() {
         assumePlatformROrAbove()
         val bounds = Bounds(1, 2, 3, 4)
         val insetsA = Insets.of(1, 2, 3, 4)
         val insetsB = Insets.of(6, 7, 8, 9)
         val builderA = WindowInsetsCompat.Builder()
         val builderB = WindowInsetsCompat.Builder()
-        for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
+        for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
             builderA.setInsets(type, insetsA)
             builderB.setInsets(type, insetsB)
         }
         val windowInsetsCompatA = builderA.build()
         val windowInsetsCompatB = builderB.build()
-        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA)
-        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB)
+        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA, 1f /* density */)
+        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB, 1f /* density */)
         assertNotEquals(windowMetricsA, windowMetricsB)
     }
 
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
index f20e341..c557551 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
@@ -33,17 +33,21 @@
 import androidx.window.core.ConsumerAdapter
 import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.core.util.function.Consumer as OEMConsumer
+import androidx.window.extensions.layout.DisplayFoldFeature
 import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
 import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
 import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
+import androidx.window.extensions.layout.SupportedWindowFeatures
 import androidx.window.extensions.layout.WindowLayoutComponent
 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.WindowMetricsCalculatorCompat
 import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter.translate
 import java.util.function.Consumer as JavaConsumer
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -645,9 +649,48 @@
         consumer.assertValues(expected)
     }
 
+    @Test
+    fun testSupportedFeatures_throwsBeforeApi6() {
+        assumeBeforeVendorApiLevel(6)
+
+        val component = FakeWindowComponent()
+        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+        assertThrows(UnsupportedOperationException::class.java) { backend.supportedPostures }
+    }
+
+    @Test
+    fun testSupportedFeatures_emptyListReturnsNoFeatures() {
+        assumeAtLeastVendorApiLevel(6)
+
+        val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf()).build()
+        val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
+        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+        val actual = backend.supportedPostures
+        assertEquals(emptyList<SupportedPosture>(), actual)
+    }
+
+    @Test
+    fun testSupportedFeatures_halfOpenedReturnsTabletopSupport() {
+        assumeAtLeastVendorApiLevel(6)
+
+        val foldFeature =
+            DisplayFoldFeature.Builder(DisplayFoldFeature.TYPE_SCREEN_FOLD_IN)
+                .addProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+                .build()
+        val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf(foldFeature)).build()
+        val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
+        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
+
+        val actual = backend.supportedPostures
+        assertEquals(listOf(SupportedPosture.TABLETOP), actual)
+    }
+
     internal companion object {
         private fun newTestOEMWindowLayoutInfo(activity: Activity): OEMWindowLayoutInfo {
-            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
+            val bounds =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
             val displayFeatures = listOf(feature)
@@ -661,7 +704,7 @@
          */
         @RequiresApi(Build.VERSION_CODES.R)
         private fun newTestOEMWindowLayoutInfo(@UiContext context: Context): OEMWindowLayoutInfo {
-            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(context).bounds
+            val bounds = WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(context).bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
             val displayFeatures = listOf(feature)
@@ -696,7 +739,8 @@
         }
     }
 
-    private class FakeWindowComponent : WindowLayoutComponent {
+    private class FakeWindowComponent(private val windowFeatures: SupportedWindowFeatures? = null) :
+        WindowLayoutComponent {
 
         val consumers = mutableListOf<JavaConsumer<OEMWindowLayoutInfo>>()
         val oemConsumers = mutableListOf<OEMConsumer<OEMWindowLayoutInfo>>()
@@ -723,6 +767,14 @@
             oemConsumers.remove(consumer)
         }
 
+        override fun getSupportedWindowFeatures(): SupportedWindowFeatures {
+            return windowFeatures
+                ?: throw UnsupportedOperationException(
+                    "Window features are not set. Either the vendor API level is too low or value " +
+                        "was not set"
+                )
+        }
+
         @SuppressLint("NewApi")
         fun emit(info: OEMWindowLayoutInfo) {
             if (ExtensionsUtil.safeVendorApiLevel < 2) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
index 3a948d3..325960a 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
@@ -32,7 +32,7 @@
 import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
 import androidx.window.layout.TestFoldingFeatureUtil.invalidNonZeroFoldBounds
 import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
+import androidx.window.layout.WindowMetricsCalculatorCompat
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
@@ -50,7 +50,8 @@
     @Test
     fun testTranslate_foldingFeature() {
         activityScenario.scenario.onActivity { activity ->
-            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
             val bounds = windowMetrics.bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
@@ -66,7 +67,8 @@
     @Test
     fun testTranslate_windowLayoutInfo() {
         activityScenario.scenario.onActivity { activity ->
-            val bounds = computeCurrentWindowMetrics(activity).bounds
+            val bounds =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
             val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
@@ -84,7 +86,8 @@
     fun testTranslate_windowLayoutInfoFromContext() {
         assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
         activityScenario.scenario.onActivity { activity ->
-            val bounds = computeCurrentWindowMetrics(activity).bounds
+            val bounds =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity).bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
             val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
@@ -101,7 +104,8 @@
     @Test
     fun testTranslate_foldingFeature_invalidType() {
         activityScenario.scenario.onActivity { activity ->
-            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
             val bounds = windowMetrics.bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, -1, STATE_HALF_OPENED)
@@ -115,7 +119,8 @@
     @Test
     fun testTranslate_foldingFeature_invalidState() {
         activityScenario.scenario.onActivity { activity ->
-            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
             val bounds = windowMetrics.bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, -1)
@@ -129,7 +134,8 @@
     @Test
     fun testTranslate_foldingFeature_invalidBounds() {
         activityScenario.scenario.onActivity { activity ->
-            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val windowMetrics =
+                WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(activity)
             val windowBounds = windowMetrics.bounds
 
             val source =
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
index 292197c..1936828 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
@@ -42,6 +42,7 @@
 import org.junit.Assert.assertNotNull
 import org.junit.Assume.assumeTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatcher
@@ -85,6 +86,7 @@
         }
     }
 
+    @Ignore("b/241174887")
     @Test
     fun testWindowLayoutCallbackOnConfigChange() {
         val testScope = TestScope(UnconfinedTestDispatcher())
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
index 69474bd..df07ffc 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
@@ -198,6 +198,13 @@
         secondConsumer.assertValues(secondExpected)
     }
 
+    @Test(expected = UnsupportedOperationException::class)
+    fun testSupportedWindowFeatures_throws() {
+        val interfaceCompat = SwitchOnUnregisterExtensionInterfaceCompat()
+        val backend = SidecarWindowBackend(interfaceCompat)
+        backend.supportedPostures
+    }
+
     internal companion object {
         private fun newTestWindowLayoutInfo(): WindowLayoutInfo {
             val feature1: DisplayFeature = HardwareFoldingFeature(Bounds(0, 2, 3, 4), HINGE, FLAT)
diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
new file mode 100644
index 0000000..00d5417
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.os.Build
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [BoundsHelper]. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class BoundsHelperTest {
+
+    @get:Rule
+    var activityScenarioRule: ActivityScenarioRule<TestActivity> =
+        ActivityScenarioRule(TestActivity::class.java)
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    @Test
+    fun testCurrentWindowBounds_postR() {
+        assumePlatformROrAbove()
+        activityScenarioRule.scenario.onActivity { activity ->
+            val currentBounds = BoundsHelper.getInstance().currentWindowBounds(activity)
+            val expectedBounds =
+                activity.getSystemService(WindowManager::class.java).currentWindowMetrics.bounds
+            assertEquals(expectedBounds, currentBounds)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    @Test
+    fun testMaximumWindowBounds_postR() {
+        assumePlatformROrAbove()
+        activityScenarioRule.scenario.onActivity { activity ->
+            val currentBounds = BoundsHelper.getInstance().maximumWindowBounds(activity)
+            val expectedBounds =
+                activity.getSystemService(WindowManager::class.java).maximumWindowMetrics.bounds
+            assertEquals(expectedBounds, currentBounds)
+        }
+    }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
new file mode 100644
index 0000000..19c568a
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.graphics.Rect
+import android.os.Build
+import android.util.DisplayMetrics
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.window.TestActivity
+import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeU
+import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
+import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
+import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for the [DensityCompatHelper] class. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class DensityCompatHelperTest {
+
+    @get:Rule
+    var activityScenarioRule: ActivityScenarioRule<TestActivity> =
+        ActivityScenarioRule(TestActivity::class.java)
+
+    @Test
+    fun testDensityFromContext_beforeU() {
+        assumePlatformBeforeU()
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val helper = DensityCompatHelper.getInstance()
+            assertEquals(activity.resources.displayMetrics.density, helper.density(activity))
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testDensityFromContext_UOrAbove() {
+        assumePlatformUOrAbove()
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val wm = activity.getSystemService(WindowManager::class.java)
+            val helper = DensityCompatHelper.getInstance()
+            assertEquals(wm.currentWindowMetrics.density, helper.density(activity))
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testDensityFromConfiguration_beforeU() {
+        assumePlatformBeforeU()
+        assumePlatformROrAbove()
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val helper = DensityCompatHelper.getInstance()
+
+            @Suppress("DEPRECATION")
+            val fakeWindowMetrics =
+                AndroidWindowMetrics(
+                    Rect(0, 0, 0, 0),
+                    WindowInsets.Builder().build(),
+                )
+
+            val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
+            val expectedDensity =
+                activity.resources.configuration.densityDpi.toFloat() /
+                    DisplayMetrics.DENSITY_DEFAULT
+            assertEquals(expectedDensity, density)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testDensityFromWindowMetrics_UOrAbove() {
+        assumePlatformUOrAbove()
+        runActionsAcrossActivityLifecycle(activityScenarioRule, {}) { activity: TestActivity ->
+            val helper = DensityCompatHelper.getInstance()
+
+            val fakeDensity = 123.456f
+            val fakeWindowMetrics =
+                AndroidWindowMetrics(Rect(0, 0, 0, 0), WindowInsets.Builder().build(), fakeDensity)
+
+            val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
+            assertEquals(fakeDensity, density)
+        }
+    }
+}
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
index 5e75a12..8778c51 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
@@ -23,7 +23,10 @@
         window:splitMinHeightDp="600"
         window:splitMinSmallestWidthDp="0"
         window:splitLayoutDirection="topToBottom"
-        window:animationBackgroundColor="#0000FF">
+        window:animationBackgroundColor="#0000FF"
+        window:splitOpenAnimation="jumpCut"
+        window:splitCloseAnimation="jumpCut"
+        window:splitChangeAnimation="jumpCut">
         <SplitPairFilter
             window:primaryActivityName="A"
             window:secondaryActivityName="B"/>
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
index df0871f..ac79a56 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
@@ -24,7 +24,10 @@
         window:splitMinHeightDp="600"
         window:splitMinSmallestWidthDp="0"
         window:splitLayoutDirection="bottomToTop"
-        window:animationBackgroundColor="@color/testColor">
+        window:animationBackgroundColor="@color/testColor"
+        window:splitOpenAnimation="jumpCut"
+        window:splitCloseAnimation="jumpCut"
+        window:splitChangeAnimation="jumpCut">
         <ActivityFilter
             window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
     </SplitPlaceholderRule>
diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
index ed346b6..ae9ce65 100644
--- a/window/window/src/main/java/androidx/window/WindowProperties.kt
+++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
@@ -109,7 +109,6 @@
      * </application>
      * ```
      */
-    // TODO(b/274924641): Make property public
     const val PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED =
         "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"
 
@@ -170,4 +169,88 @@
      */
     const val PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES =
         "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
+
+    /**
+     * Application-level [PackageManager][android.content.pm.PackageManager.Property] tag that (when
+     * set to false) informs the system the app has opted out of the user-facing aspect ratio
+     * compatibility override.
+     *
+     * The compatibility override enables device users to set the app's aspect ratio or force the
+     * app to fill the display regardless of the aspect ratio or orientation specified in the app
+     * manifest.
+     *
+     * The aspect ratio compatibility override is exposed to users in device settings. A menu in
+     * device settings lists all apps that have not opted out of the compatibility override. Users
+     * select apps from the menu and set the app aspect ratio on a per-app basis. Typically, the
+     * menu is available only on large screen devices.
+     *
+     * When users apply the aspect ratio override, the minimum aspect ratio specified in the app
+     * manifest is overridden. If users choose a full-screen aspect ratio, the orientation of the
+     * activity is forced to
+     * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER]; see
+     * [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE] to disable the full-screen
+     * option only.
+     *
+     * The user override is intended to improve the app experience on devices that have the ignore
+     * orientation request display setting enabled by OEMs (enables compatibility mode for fixed
+     * orientation on Android 12 (API level 31) or higher; see
+     * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
+     * for more details).
+     *
+     * To opt out of the user aspect ratio compatibility override, add this property to your app
+     * manifest and set the value to `false`. Your app will be excluded from the list of apps in
+     * device settings, and users will not be able to override the app's aspect ratio.
+     *
+     * Not setting this property at all, or setting this property to `true` has no effect.
+     *
+     * **Syntax:**
+     *
+     * ```
+     * <application>
+     *   <property
+     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+     *     android:value="false" />
+     * </application>
+     * ```
+     */
+    const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE =
+        "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
+
+    /**
+     * Application-level [PackageManager][android.content.pm.PackageManager.Property] tag that (when
+     * set to false) informs the system the app has opted out of the full-screen option of the user
+     * aspect ratio compatibility override settings. (For background information about the user
+     * aspect ratio compatibility override, see [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE].)
+     *
+     * When users apply the full-screen compatibility override, the orientation of the activity is
+     * forced to [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER].
+     *
+     * The user override aims to improve the app experience on devices that have the ignore
+     * orientation request display setting enabled by OEMs (enables compatibility mode for fixed
+     * orientation on Android 12 (API level 31) or higher; see
+     * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
+     * for more details).
+     *
+     * To opt out of the full-screen option of the user aspect ratio compatibility override, add
+     * this property to your app manifest and set the value to `false`. Your app will have
+     * full-screen option removed from the list of user aspect ratio override options in device
+     * settings, and users will not be able to apply full-screen override to your app.
+     *
+     * **Note:** If [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE] is `false`, this property has
+     * no effect.
+     *
+     * Not setting this property at all, or setting this property to `true` has no effect.
+     *
+     * **Syntax:**
+     *
+     * ```
+     * <application>
+     *   <property
+     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
+     *     android:value="false" />
+     * </application>
+     * ```
+     */
+    const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE =
+        "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
 }
diff --git a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
index 97fd83d..a78df6b 100644
--- a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
+++ b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
@@ -17,6 +17,7 @@
 package androidx.window
 
 import androidx.annotation.IntRange
+import androidx.annotation.RestrictTo
 import androidx.window.core.ExtensionsUtil
 
 /**
@@ -31,7 +32,7 @@
  *
  * @sample androidx.window.samples.checkWindowSdkExtensionsVersion
  */
-abstract class WindowSdkExtensions internal constructor() {
+abstract class WindowSdkExtensions @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor() {
 
     /**
      * Reports the device's extension version
@@ -56,6 +57,25 @@
         }
     }
 
+    /**
+     * Checks the [extensionVersion] and throws [UnsupportedOperationException] if the version is
+     * not in the [range].
+     *
+     * This is useful to provide compatibility for APIs updated in 2+ but deprecated in latest
+     * version.
+     *
+     * @param range the required extension range of the targeting API.
+     * @throws UnsupportedOperationException if the required [range] is not satisfied.
+     */
+    internal fun requireExtensionVersion(range: kotlin.ranges.IntRange) {
+        if (extensionVersion !in range) {
+            throw UnsupportedOperationException(
+                "This API requires extension version " +
+                    "$range, but the device is on $extensionVersion"
+            )
+        }
+    }
+
     companion object {
         /** Returns a [WindowSdkExtensions] instance. */
         @JvmStatic
@@ -65,17 +85,20 @@
 
         private var decorator: WindowSdkExtensionsDecorator = EmptyDecoratorWindowSdk
 
-        internal fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
             decorator = overridingDecorator
         }
 
-        internal fun reset() {
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        fun reset() {
             decorator = EmptyDecoratorWindowSdk
         }
     }
 }
 
-internal interface WindowSdkExtensionsDecorator {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface WindowSdkExtensionsDecorator {
     /** Returns a [WindowSdkExtensions] instance. */
     fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions
 }
diff --git a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
index e0e9574..8734411 100644
--- a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import android.view.View
+import android.view.Window
+import androidx.window.area.utils.PresentationWindowCompatUtils
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.extensions.area.ExtensionWindowAreaPresentation
 import androidx.window.extensions.area.WindowAreaComponent
@@ -25,11 +27,16 @@
 @ExperimentalWindowApi
 internal class RearDisplayPresentationSessionPresenterImpl(
     private val windowAreaComponent: WindowAreaComponent,
-    private val presentation: ExtensionWindowAreaPresentation
+    private val presentation: ExtensionWindowAreaPresentation,
+    vendorApiLevel: Int
 ) : WindowAreaSessionPresenter {
 
     override val context: Context = presentation.presentationContext
 
+    override val window: Window? =
+        if (vendorApiLevel >= 4) presentation.window
+        else PresentationWindowCompatUtils.getWindowBeforeVendorApiLevel4(presentation)
+
     override fun setContentView(view: View) {
         presentation.setPresentationView(view)
     }
diff --git a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
index 84b52d0..7ea1d32 100644
--- a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
@@ -15,6 +15,7 @@
  */
 package androidx.window.area
 
+import android.os.Build
 import androidx.window.SafeWindowExtensionsProvider
 import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid
 import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaStatusValid
@@ -41,6 +42,7 @@
                 if (
                     windowExtensions != null &&
                         isWindowAreaProviderValid(windowExtensions) &&
+                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
                         isWindowAreaComponentValid(
                             windowAreaComponentClass,
                             ExtensionsUtil.safeVendorApiLevel
@@ -49,11 +51,16 @@
                             extensionWindowAreaStatusClass,
                             ExtensionsUtil.safeVendorApiLevel
                         ) &&
-                        isValidExtensionWindowPresentation()
-                )
+                        isExtensionWindowAreaPresentationValid(
+                            extensionWindowAreaPresentationClass,
+                            ExtensionsUtil.safeVendorApiLevel
+                        )
+                ) {
                     windowExtensions.windowAreaComponent
-                else null
-            } catch (e: Exception) {
+                } else {
+                    null
+                }
+            } catch (e: Throwable) {
                 null
             }
         }
@@ -67,15 +74,6 @@
         }
     }
 
-    private fun isValidExtensionWindowPresentation(): Boolean {
-        // Not required for API Level 2 or below
-        return ExtensionsUtil.safeVendorApiLevel <= 2 ||
-            isExtensionWindowAreaPresentationValid(
-                extensionWindowAreaPresentationClass,
-                ExtensionsUtil.safeVendorApiLevel
-            )
-    }
-
     private val windowAreaComponentClass: Class<*>
         get() {
             return loader.loadClass(WindowExtensionsConstants.WINDOW_AREA_COMPONENT_CLASS)
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
index 90c9759..6d85d20 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -23,7 +23,6 @@
 import androidx.annotation.RestrictTo
 import androidx.window.WindowSdkExtensions
 import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
-import androidx.window.area.utils.DeviceUtils
 import androidx.window.core.BuildConfig
 import androidx.window.core.ExperimentalWindowApi
 import androidx.window.core.ExtensionsUtil
@@ -147,17 +146,16 @@
                     }
                     null
                 }
+
             val deviceSupported =
                 Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
                     windowAreaComponentExtensions != null &&
-                    (ExtensionsUtil.safeVendorApiLevel >= 3 ||
-                        DeviceUtils.hasDeviceMetrics(Build.MANUFACTURER, Build.MODEL))
+                    ExtensionsUtil.safeVendorApiLevel >= 3
 
             val controller =
                 if (deviceSupported) {
                     WindowAreaControllerImpl(
-                        windowAreaComponentExtensions!!,
-                        ExtensionsUtil.safeVendorApiLevel
+                        windowAreaComponent = windowAreaComponentExtensions!!,
                     )
                 } else {
                     EmptyWindowAreaControllerImpl()
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
index c8396a0..121e608 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -21,14 +21,15 @@
 import android.os.Build
 import android.util.Log
 import androidx.annotation.RequiresApi
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNKNOWN
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
 import androidx.window.area.adapter.WindowAreaAdapter
-import androidx.window.area.utils.DeviceUtils
 import androidx.window.core.BuildConfig
 import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
 import androidx.window.core.VerificationMode
 import androidx.window.extensions.area.ExtensionWindowAreaStatus
 import androidx.window.extensions.area.WindowAreaComponent
@@ -57,10 +58,10 @@
  * functionality.
  */
 @ExperimentalWindowApi
+@RequiresWindowSdkExtension(3)
 @RequiresApi(Build.VERSION_CODES.Q)
 internal class WindowAreaControllerImpl(
     private val windowAreaComponent: WindowAreaComponent,
-    private val vendorApiLevel: Int
 ) : WindowAreaController {
 
     private lateinit var rearDisplaySessionConsumer: Consumer2<Int>
@@ -89,40 +90,24 @@
                     }
 
                 windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
-                if (vendorApiLevel > 2) {
-                    windowAreaComponent.addRearDisplayPresentationStatusListener(
-                        rearDisplayPresentationListener
-                    )
-                }
+                windowAreaComponent.addRearDisplayPresentationStatusListener(
+                    rearDisplayPresentationListener
+                )
 
                 awaitClose {
                     windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
-                    if (vendorApiLevel > 2) {
-                        windowAreaComponent.removeRearDisplayPresentationStatusListener(
-                            rearDisplayPresentationListener
-                        )
-                    }
+                    windowAreaComponent.removeRearDisplayPresentationStatusListener(
+                        rearDisplayPresentationListener
+                    )
                 }
             }
         }
 
     private fun updateRearDisplayAvailability(status: @WindowAreaComponent.WindowAreaStatus Int) {
         val windowMetrics =
-            if (vendorApiLevel >= 3) {
-                WindowMetricsCalculator.fromDisplayMetrics(
-                    displayMetrics = windowAreaComponent.rearDisplayMetrics
-                )
-            } else {
-                val displayMetrics =
-                    DeviceUtils.getRearDisplayMetrics(Build.MANUFACTURER, Build.MODEL)
-                if (displayMetrics != null) {
-                    WindowMetricsCalculator.fromDisplayMetrics(displayMetrics = displayMetrics)
-                } else {
-                    throw IllegalArgumentException(
-                        "DeviceUtils rear display metrics entry should not be null"
-                    )
-                }
-            }
+            WindowMetricsCalculator.fromDisplayMetrics(
+                displayMetrics = windowAreaComponent.rearDisplayMetrics
+            )
 
         currentRearDisplayModeStatus = WindowAreaAdapter.translate(status, activeWindowAreaSession)
         updateRearDisplayWindowArea(
@@ -393,7 +378,8 @@
                             windowAreaPresentationSessionCallback.onSessionStarted(
                                 RearDisplayPresentationSessionPresenterImpl(
                                     windowAreaComponent,
-                                    windowAreaComponent.rearDisplayPresentation!!
+                                    windowAreaComponent.rearDisplayPresentation!!,
+                                    ExtensionsUtil.safeVendorApiLevel
                                 )
                             )
                         }
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
index af47bd5..14620ae 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
@@ -22,6 +22,7 @@
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
 import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
 import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.area.WindowAreaComponent
 import androidx.window.layout.WindowMetrics
 
@@ -86,7 +87,8 @@
             OPERATION_PRESENT_ON_AREA ->
                 RearDisplayPresentationSessionPresenterImpl(
                     windowAreaComponent,
-                    windowAreaComponent.rearDisplayPresentation!!
+                    windowAreaComponent.rearDisplayPresentation!!,
+                    ExtensionsUtil.safeVendorApiLevel
                 )
             else -> {
                 throw IllegalArgumentException("Invalid operation provided")
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
index cd45d92..c9b7a01 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.view.View
+import android.view.Window
 import androidx.window.core.ExperimentalWindowApi
 
 /**
@@ -34,6 +35,15 @@
     val context: Context
 
     /**
+     * Returns the [Window] associated with the active presentation window area or null if there is
+     * no [Window] currently active. This could occur if the presenter has already been dismissed,
+     * and there is no expectation that the [Window] would become non-null at a later point. This
+     * API can be used to directly access parts of the [Window] API that are not available through
+     * the [Context] provided.
+     */
+    val window: Window?
+
+    /**
      * Sets a [View] to show on a window area. After setting the view the system can turn on the
      * corresponding display and start showing content.
      */
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
deleted file mode 100644
index 0ab78c0..0000000
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.reflectionguard;
-
-import android.app.Activity;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.window.extensions.area.WindowAreaComponent;
-import androidx.window.extensions.core.util.function.Consumer;
-
-/**
- * This file defines the Vendor API Level 2 Requirements for WindowAreaComponent. This is used
- * in the client library to perform reflection guard to ensure that the OEM extension implementation
- * is complete.
- *
- * @see WindowAreaComponent
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface WindowAreaComponentApi2Requirements {
-
-    /** @see WindowAreaComponent#addRearDisplayStatusListener */
-    void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
-
-    /** @see WindowAreaComponent#removeRearDisplayStatusListener */
-    void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
-
-    /** @see WindowAreaComponent#startRearDisplaySession */
-    void startRearDisplaySession(@NonNull Activity activity,
-            @NonNull Consumer<Integer> consumer);
-
-    /** @see WindowAreaComponent#endRearDisplaySession */
-    void endRearDisplaySession();
-}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
index 56c54aa..eab11a7 100644
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -36,7 +36,20 @@
  * @see WindowAreaComponent
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public interface WindowAreaComponentApi3Requirements extends WindowAreaComponentApi2Requirements {
+public interface WindowAreaComponentApi3Requirements {
+
+    /** @see WindowAreaComponent#addRearDisplayStatusListener */
+    void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#removeRearDisplayStatusListener */
+    void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#startRearDisplaySession */
+    void startRearDisplaySession(@NonNull Activity activity,
+            @NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#endRearDisplaySession */
+    void endRearDisplaySession();
 
     /** @see WindowAreaComponent#addRearDisplayPresentationStatusListener */
     void addRearDisplayPresentationStatusListener(
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
index 1ab5cdd..fe135be 100644
--- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
@@ -24,46 +24,47 @@
 internal object WindowAreaComponentValidator {
 
     internal fun isWindowAreaComponentValid(windowAreaComponent: Class<*>, apiLevel: Int): Boolean {
-        return when {
-            apiLevel <= 1 -> false
-            apiLevel == 2 ->
-                validateImplementation(
-                    windowAreaComponent,
-                    WindowAreaComponentApi2Requirements::class.java
-                )
-            else ->
-                validateImplementation(
-                    windowAreaComponent,
-                    WindowAreaComponentApi3Requirements::class.java
-                )
-        }
+        val isWindowAreaComponentValid: Boolean =
+            when {
+                apiLevel <= 2 -> false
+                else ->
+                    validateImplementation(
+                        windowAreaComponent,
+                        WindowAreaComponentApi3Requirements::class.java
+                    )
+            }
+        return isWindowAreaComponentValid
     }
 
     internal fun isExtensionWindowAreaStatusValid(
         extensionWindowAreaStatus: Class<*>,
         apiLevel: Int
     ): Boolean {
-        return when {
-            apiLevel <= 1 -> false
-            else ->
-                validateImplementation(
-                    extensionWindowAreaStatus,
-                    ExtensionWindowAreaStatusRequirements::class.java
-                )
-        }
+        val isExtensionWindowAreaStatusValid: Boolean =
+            when {
+                apiLevel <= 2 -> false
+                else ->
+                    validateImplementation(
+                        extensionWindowAreaStatus,
+                        ExtensionWindowAreaStatusRequirements::class.java
+                    )
+            }
+        return isExtensionWindowAreaStatusValid
     }
 
     internal fun isExtensionWindowAreaPresentationValid(
         extensionWindowAreaPresentation: Class<*>,
         apiLevel: Int
     ): Boolean {
-        return when {
-            apiLevel <= 2 -> false
-            else ->
-                validateImplementation(
-                    extensionWindowAreaPresentation,
-                    ExtensionWindowAreaPresentation::class.java
-                )
-        }
+        val isExtensionWindowAreaPresentationValid: Boolean =
+            when {
+                apiLevel <= 2 -> false
+                else ->
+                    validateImplementation(
+                        extensionWindowAreaPresentation,
+                        ExtensionWindowAreaPresentation::class.java
+                    )
+            }
+        return isExtensionWindowAreaPresentationValid
     }
 }
diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
deleted file mode 100644
index 359e957..0000000
--- a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.utils
-
-import android.util.DisplayMetrics
-
-/** Data class holding metrics about a specific device. */
-internal class DeviceMetrics(
-    val manufacturer: String,
-    val model: String,
-    val rearDisplayMetrics: DisplayMetrics
-) {
-    override fun equals(other: Any?): Boolean {
-        return other is DeviceMetrics &&
-            manufacturer == other.manufacturer &&
-            model == other.model &&
-            rearDisplayMetrics.equals(other.rearDisplayMetrics)
-    }
-
-    override fun hashCode(): Int {
-        var result = manufacturer.hashCode()
-        result = 31 * result + model.hashCode()
-        result = 31 * result + rearDisplayMetrics.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "DeviceMetrics{ Manufacturer: $manufacturer, model: $model, " +
-            "Rear display metrics: $rearDisplayMetrics }"
-    }
-}
diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
deleted file mode 100644
index 33f241d..0000000
--- a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.utils
-
-import android.util.DisplayMetrics
-import java.util.Locale
-
-/**
- * Utility object to provide information about specific devices that may not be available through
- * the extensions API at a certain vendor API level
- */
-internal object DeviceUtils {
-
-    private val deviceList =
-        listOf(
-            DeviceMetrics(
-                "google",
-                "pixel fold",
-                DisplayMetrics().apply {
-                    widthPixels = 1080
-                    heightPixels = 2092
-                    density = 2.625f
-                    densityDpi = DisplayMetrics.DENSITY_420
-                }
-            )
-        )
-
-    internal fun hasDeviceMetrics(manufacturer: String, model: String): Boolean {
-        return deviceList.any {
-            it.manufacturer == manufacturer.lowercase(Locale.US) &&
-                it.model == model.lowercase(Locale.US)
-        }
-    }
-
-    internal fun getRearDisplayMetrics(manufacturer: String, model: String): DisplayMetrics? {
-        return deviceList
-            .firstOrNull {
-                it.manufacturer == manufacturer.lowercase(Locale.US) &&
-                    it.model == model.lowercase(Locale.US)
-            }
-            ?.rearDisplayMetrics
-    }
-}
diff --git a/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
new file mode 100644
index 0000000..593fb4c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.utils
+
+import android.annotation.SuppressLint
+import android.view.Window
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import java.lang.reflect.Method
+
+internal object PresentationWindowCompatUtils {
+
+    // We perform our own extensions vendor API level check at the call-site
+    @SuppressLint("BanUncheckedReflection")
+    fun getWindowBeforeVendorApiLevel4(
+        extensionPresentation: ExtensionWindowAreaPresentation
+    ): Window? {
+        val getWindowMethod = getWindowMethod(extensionPresentation)
+        return if (getWindowMethod == null) null
+        else (getWindowMethod.invoke(extensionPresentation) as Window?)
+    }
+
+    private fun getWindowMethod(extensionPresentation: ExtensionWindowAreaPresentation): Method? {
+        return extensionPresentation.javaClass.methods.firstOrNull { method: Method? ->
+            method?.name == "getWindow" && method.returnType == android.view.Window::class.java
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/core/Bounds.kt b/window/window/src/main/java/androidx/window/core/Bounds.kt
index f821baa..e31ed65 100644
--- a/window/window/src/main/java/androidx/window/core/Bounds.kt
+++ b/window/window/src/main/java/androidx/window/core/Bounds.kt
@@ -28,10 +28,10 @@
  * contain any behavior or calculations.
  */
 internal class Bounds(
-    public val left: Int,
-    public val top: Int,
-    public val right: Int,
-    public val bottom: Int
+    val left: Int,
+    val top: Int,
+    val right: Int,
+    val bottom: Int,
 ) {
     constructor(rect: Rect) : this(rect.left, rect.top, rect.right, rect.bottom)
 
@@ -88,4 +88,8 @@
         result = 31 * result + bottom
         return result
     }
+
+    companion object {
+        val EMPTY_BOUNDS = Bounds(0, 0, 0, 0)
+    }
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
index 32c6e07..3f6655e 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
@@ -17,11 +17,14 @@
 package androidx.window.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
 import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
+import androidx.core.util.Consumer
 import androidx.window.RequiresWindowSdkExtension
-import androidx.window.core.ExperimentalWindowApi
+import androidx.window.WindowSdkExtensions
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
 
 /** The controller that allows checking the current [Activity] embedding status. */
 class ActivityEmbeddingController internal constructor(private val backend: EmbeddingBackend) {
@@ -31,7 +34,6 @@
      *
      * @param activity the [Activity] to check.
      */
-    // TODO(b/204399167) Migrate to a Flow
     fun isActivityEmbedded(activity: Activity): Boolean = backend.isActivityEmbedded(activity)
 
     /**
@@ -39,27 +41,136 @@
      * embedding container and associated with a [SplitInfo]. Returns `null` if there is no such
      * [ActivityStack].
      *
+     * Started from [WindowSdkExtensions.extensionVersion] 5, this method can also obtain standalone
+     * [ActivityStack], which is not associated with any [SplitInfo]. For example, an
+     * [ActivityStack] launched with [ActivityRule.alwaysExpand], or an overlay [ActivityStack]
+     * launched by [setLaunchingActivityStack] with [OverlayCreateParams].
+     *
      * @param activity The [Activity] to check.
      * @return the [ActivityStack] that this [activity] is part of, or `null` if there is no such
      *   [ActivityStack].
      */
-    @ExperimentalWindowApi
     fun getActivityStack(activity: Activity): ActivityStack? = backend.getActivityStack(activity)
 
     /**
-     * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].
+     * Sets the launching [ActivityStack] to the given [Bundle].
      *
-     * @param options The [android.app.ActivityOptions] to be updated.
-     * @param token The token of the [ActivityStack] to be set.
+     * Apps can launch an [Activity] into the [ActivityStack] by [Activity.startActivity].
+     *
+     * @param options the [Bundle] to be updated.
+     * @param activityStack the [ActivityStack] to be set.
+     */
+    @RequiresWindowSdkExtension(5)
+    internal fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle =
+        backend.setLaunchingActivityStack(options, activityStack)
+
+    /**
+     * Finishes a set of [activityStacks][ActivityStack] from the lowest to the highest z-order
+     * regardless of the order of `activityStack` passed in the input parameter.
+     *
+     * If a remaining activityStack from a split participates in other splits with other
+     * activityStacks, the remaining activityStack might split with other activityStacks. For
+     * example, if activityStack A splits with activityStack B and C, and activityStack C covers
+     * activityStack B, finishing activityStack C might make the split of activityStack A and B
+     * show.
+     *
+     * If all split-associated activityStacks are finished, the remaining activityStack will be
+     * expanded to fill the parent task container. This is useful to expand the primary container as
+     * the sample linked below shows.
+     *
+     * **Note** that it's caller's responsibility to check whether this API is supported by checking
+     * [WindowSdkExtensions.extensionVersion] is greater than or equal to 5. If not, an alternative
+     * approach to finishing all containers above a particular activity can be to launch it again
+     * with flag [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].
+     *
+     * @param activityStacks The set of [ActivityStack] to be finished.
+     * @throws UnsupportedOperationException if extension version is less than 5.
+     * @sample androidx.window.samples.embedding.expandPrimaryContainer
+     */
+    @RequiresWindowSdkExtension(5)
+    fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+        backend.finishActivityStacks(activityStacks)
+    }
+
+    /**
+     * Sets the [EmbeddingConfiguration] of the Activity Embedding environment that defines how the
+     * embedded Activities behaves.
+     *
+     * The [EmbeddingConfiguration] can be supported only if the vendor API level of the target
+     * device is equals or higher than required API level. Otherwise, it would be no-op when setting
+     * the [EmbeddingConfiguration] on a target device that has lower API level.
+     *
+     * In addition, the existing configuration in the library won't be overwritten if the properties
+     * of the given [embeddingConfiguration] are undefined. Only the configuration properties that
+     * are explicitly set will be updated.
+     *
+     * **Note** that it is recommended to be configured in the [androidx.startup.Initializer] or
+     * [android.app.Application.onCreate], so that the [EmbeddingConfiguration] is applied early in
+     * the application startup, before any activities complete initialization. The
+     * [EmbeddingConfiguration] updates afterward may or may not apply to already running
+     * activities.
+     *
+     * @param embeddingConfiguration The [EmbeddingConfiguration]
+     */
+    @RequiresWindowSdkExtension(5)
+    fun setEmbeddingConfiguration(embeddingConfiguration: EmbeddingConfiguration) {
+        backend.setEmbeddingConfiguration(embeddingConfiguration)
+    }
+
+    /**
+     * Triggers calculator functions set through [SplitController.setSplitAttributesCalculator] and
+     * [OverlayController.setOverlayAttributesCalculator] to update attributes for visible
+     * [activityStacks][ActivityStack].
+     *
+     * This method can be used when the application wants to update the embedding presentation based
+     * on the application state.
+     *
+     * This method is not needed for changes that are driven by window and device state changes or
+     * new activity starts, because those will invoke the calculator functions automatically.
+     *
+     * Visible [activityStacks][ActivityStack] are usually the last element of [SplitInfo] list
+     * which was received from the callback registered in [SplitController.splitInfoList] and an
+     * active overlay [ActivityStack] if exists.
+     *
+     * The call will be no-op if there is no visible [activityStacks][ActivityStack] or there's no
+     * calculator set.
+     *
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 3.
+     * @see androidx.window.embedding.OverlayController.setOverlayAttributesCalculator
+     * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
      */
     @RequiresWindowSdkExtension(3)
-    internal fun setLaunchingActivityStack(
-        options: ActivityOptions,
-        token: IBinder
-    ): ActivityOptions {
-        return backend.setLaunchingActivityStack(options, token)
+    fun invalidateVisibleActivityStacks() {
+        backend.invalidateVisibleActivityStacks()
     }
 
+    /**
+     * A [Flow] of [EmbeddedActivityWindowInfo] that reports the change to the embedded window
+     * related info of the [activity].
+     *
+     * The [Flow] will immediately be invoked with the latest value upon registration if the
+     * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
+     *
+     * When the [activity] is embedded, the [Flow] will be invoked when [EmbeddedActivityWindowInfo]
+     * is changed. When the [activity] is not embedded, the [Flow] will not be triggered unless the
+     * [activity] is becoming non-embedded from embedded.
+     *
+     * Note that this API is only supported on the device with
+     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+     * [WindowSdkExtensions.extensionVersion] is less than 6, this [Flow] will not be invoked.
+     *
+     * @param activity the [Activity] that is interested in getting the embedded window info.
+     * @return a [Flow] of [EmbeddedActivityWindowInfo] of the [activity].
+     */
+    @RequiresWindowSdkExtension(6)
+    fun embeddedActivityWindowInfo(activity: Activity): Flow<EmbeddedActivityWindowInfo> =
+        callbackFlow {
+            val callback = Consumer { info: EmbeddedActivityWindowInfo -> trySend(info) }
+            backend.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
+            awaitClose { backend.removeEmbeddedActivityWindowInfoCallbackForActivity(callback) }
+        }
+
     companion object {
         /**
          * Obtains an instance of [ActivityEmbeddingController].
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
new file mode 100644
index 0000000..b3581ef
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:JvmName("ActivityEmbeddingOptions")
+
+package androidx.window.embedding
+
+import android.app.Activity
+import android.app.ActivityOptions
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+
+/**
+ * Sets the target launching [ActivityStack] to the given [Bundle].
+ *
+ * The [Bundle] then could be used to launch an [Activity] to the top of the [ActivityStack] through
+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle], it's
+ * suggested to use the bundle to call this method.
+ *
+ * It is suggested to use a visible [ActivityStack] reported by [SplitController.splitInfoList] or
+ * [OverlayController.overlayInfo], or the launching activity will be launched on the default target
+ * if the [activityStack] no longer exists in the host task. The default target could be the top of
+ * the visible split's secondary [ActivityStack], or the top of the host task.
+ *
+ * Below samples are use cases to specify the launching [ActivityStack].
+ *
+ * @sample androidx.window.samples.embedding.launchingOnPrimaryActivityStack
+ * @sample androidx.window.samples.embedding.launchingOnOverlayActivityStack
+ * @param context The [android.content.Context] that is going to be used for launching activity with
+ *   this [Bundle], which is usually be the [android.app.Activity] of the app that hosts the task.
+ * @param activityStack The target [ActivityStack] for launching.
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 5.
+ */
+@RequiresWindowSdkExtension(5)
+fun Bundle.setLaunchingActivityStack(context: Context, activityStack: ActivityStack): Bundle =
+    ActivityEmbeddingController.getInstance(context).setLaunchingActivityStack(this, activityStack)
+
+/**
+ * Puts [OverlayCreateParams] to [Bundle] to create a singleton-per-task overlay [ActivityStack].
+ *
+ * The [Bundle] then could be used to launch an [Activity] to the [ActivityStack] through
+ * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
+ * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle], it's
+ * suggested to use the bundle to call this method.
+ *
+ * Below sample shows how to launch an overlay [ActivityStack].
+ *
+ * If there's an existing overlay [ActivityStack] shown, the existing overlay container may be
+ * dismissed or updated based on [OverlayCreateParams.tag] and [activity] because of following
+ * constraints:
+ * 1. A task can hold only one overlay container at most.
+ * 2. An overlay [ActivityStack] tag is unique per process.
+ *
+ * Belows are possible scenarios:
+ * 1. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in the same
+ *    task as [activity], the overlay container's [OverlayAttributes] will be updated to
+ *    [OverlayCreateParams.overlayAttributes], and the activity will be launched on the top of the
+ *    overlay [ActivityStack].
+ * 2. If there's an overlay container with different `tag` from [OverlayCreateParams.tag] in the
+ *    same task as [activity], the existing overlay container will be dismissed, and a new overlay
+ *    container will be launched with the new [OverlayCreateParams].
+ * 3. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in a
+ *    different task from [activity], the existing overlay container in the other task will be
+ *    dismissed, and a new overlay container will be launched in the task of [activity].
+ *
+ * Note that the second and the third scenarios may happen at the same time if [activity]'s task
+ * holds an overlay container and [OverlayCreateParams.tag] matches an overlay container in a
+ * different task.
+ *
+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
+ * @param activity The [Activity] that is going to be used for launching activity with this
+ *   [ActivityOptions], which is usually be the [Activity] of the app that hosts the task.
+ * @param overlayCreateParams The parameter container to create an overlay [ActivityStack]
+ * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 6.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+fun Bundle.setOverlayCreateParams(
+    activity: Activity,
+    overlayCreateParams: OverlayCreateParams
+): Bundle =
+    OverlayController.getInstance(activity).setOverlayCreateParams(this, overlayCreateParams)
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
new file mode 100644
index 0000000..34b71fe
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.os.Bundle
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.EmbeddingBounds.Dimension
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG
+import androidx.window.extensions.embedding.ActivityStack.Token
+
+/**
+ * The implementation of ActivityEmbeddingOptions in WM Jetpack which uses constants defined in
+ * [androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties] and this object.
+ */
+internal object ActivityEmbeddingOptionsImpl {
+
+    /**
+     * Key of [EmbeddingBounds].
+     *
+     * Type: [Bundle]
+     *
+     * Properties:
+     *
+     * | Key                              | Type     | Property                    |
+     * |----------------------------------|----------|-----------------------------|
+     * | [KEY_EMBEDDING_BOUNDS_ALIGNMENT] | [Int]    | [EmbeddingBounds.alignment] |
+     * | [KEY_EMBEDDING_BOUNDS_WIDTH]     | [Bundle] | [EmbeddingBounds.width]     |
+     * | [KEY_EMBEDDING_BOUNDS_HEIGHT]    | [Bundle] | [EmbeddingBounds.height]    |
+     */
+    private const val KEY_EMBEDDING_BOUNDS = "androidx.window.embedding.EmbeddingBounds"
+
+    /**
+     * Key of [EmbeddingBounds.alignment].
+     *
+     * Type: [Int]
+     *
+     * Valid values are:
+     * - 0: [EmbeddingBounds.Alignment.ALIGN_LEFT]
+     * - 1: [EmbeddingBounds.Alignment.ALIGN_TOP]
+     * - 2: [EmbeddingBounds.Alignment.ALIGN_RIGHT]
+     * - 3: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]
+     */
+    private const val KEY_EMBEDDING_BOUNDS_ALIGNMENT =
+        "androidx.window.embedding.EmbeddingBounds.alignment"
+
+    /**
+     * Key of [EmbeddingBounds.width].
+     *
+     * Type: [Bundle] with [putDimension]
+     *
+     * Properties:
+     *
+     * | Key                                    | Type           | Property            |
+     * |----------------------------------------|----------------|---------------------|
+     * | [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]  | [String]       | The dimension type  |
+     * | [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] | [Int], [Float] | The dimension value |
+     */
+    private const val KEY_EMBEDDING_BOUNDS_WIDTH = "androidx.window.embedding.EmbeddingBounds.width"
+
+    /**
+     * Key of [EmbeddingBounds.width].
+     *
+     * Type: [Bundle] with [putDimension]
+     *
+     * Properties:
+     *
+     * | Key                                    | Type           | Property            |
+     * |----------------------------------------|----------------|---------------------|
+     * | [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]  | [String]       | The dimension type  |
+     * | [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] | [Int], [Float] | The dimension value |
+     */
+    private const val KEY_EMBEDDING_BOUNDS_HEIGHT =
+        "androidx.window.embedding.EmbeddingBounds.height"
+
+    /**
+     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+     * [Dimension] is [DIMENSION_EXPANDED].
+     */
+    private const val DIMENSION_TYPE_EXPANDED = "expanded"
+
+    /**
+     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+     * [Dimension] is [DIMENSION_HINGE].
+     */
+    private const val DIMENSION_TYPE_HINGE = "hinge"
+
+    /**
+     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+     * [Dimension] is from [Dimension.ratio]. If this type is used,
+     * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Float] value.
+     */
+    private const val DIMENSION_TYPE_RATIO = "ratio"
+
+    /**
+     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates the
+     * [Dimension] is from [Dimension.pixel]. If this type is used,
+     * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Int] value.
+     */
+    private const val DIMENSION_TYPE_PIXEL = "pixel"
+
+    /**
+     * Key of [EmbeddingBounds.Dimension] type.
+     *
+     * Type: [String]
+     *
+     * Valid values are:
+     * - [DIMENSION_TYPE_EXPANDED]: [DIMENSION_EXPANDED]
+     * - [DIMENSION_TYPE_HINGE]: [DIMENSION_HINGE]
+     * - [DIMENSION_TYPE_RATIO]: [Dimension.ratio]
+     * - [DIMENSION_TYPE_PIXEL]: [Dimension.pixel]
+     */
+    private const val KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE =
+        "androidx.window.embedding.EmbeddingBounds.dimension_type"
+
+    /**
+     * Key of [EmbeddingBounds.Dimension] value.
+     *
+     * Type: [Float] or [Int]
+     *
+     * The value passed in [Dimension.pixel] or [Dimension.ratio]:
+     * - Accept [Float] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_RATIO]
+     * - Accept [Int] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_PIXEL]
+     */
+    private const val KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE =
+        "androidx.window.embedding.EmbeddingBounds.dimension_value"
+
+    /**
+     * Key of [ActivityStack] alignment relative to the parent container.
+     *
+     * Type: [Int]
+     *
+     * Valid values are:
+     * - 0: [EmbeddingBounds.Alignment.ALIGN_LEFT]
+     * - 1: [EmbeddingBounds.Alignment.ALIGN_TOP]
+     * - 2: [EmbeddingBounds.Alignment.ALIGN_RIGHT]
+     * - 3: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]
+     */
+    private const val KEY_ACTIVITY_STACK_ALIGNMENT =
+        "androidx.window.embedding.ActivityStackAlignment"
+
+    /**
+     * Puts [OverlayCreateParams] information to [android.app.ActivityOptions] bundle to launch the
+     * overlay container
+     *
+     * @param overlayCreateParams The [OverlayCreateParams] to launch the overlay container
+     */
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    internal fun setOverlayCreateParams(
+        options: Bundle,
+        overlayCreateParams: OverlayCreateParams,
+    ) {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        options.putString(KEY_OVERLAY_TAG, overlayCreateParams.tag)
+        options.putEmbeddingBounds(overlayCreateParams.overlayAttributes.bounds)
+    }
+
+    /** Puts [EmbeddingBounds] information into a bundle for tracking. */
+    private fun Bundle.putEmbeddingBounds(embeddingBounds: EmbeddingBounds) {
+        putBundle(
+            KEY_EMBEDDING_BOUNDS,
+            Bundle().apply {
+                putInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT, embeddingBounds.alignment.value)
+                putDimension(KEY_EMBEDDING_BOUNDS_WIDTH, embeddingBounds.width)
+                putDimension(KEY_EMBEDDING_BOUNDS_HEIGHT, embeddingBounds.height)
+            }
+        )
+    }
+
+    /**
+     * Puts the alignment of the overlay [ActivityStack] relative to its parent container.
+     *
+     * It could be used as a hint of the open/close animation direction.
+     */
+    internal fun Bundle.putActivityStackAlignment(embeddingBounds: EmbeddingBounds) {
+        putInt(KEY_ACTIVITY_STACK_ALIGNMENT, embeddingBounds.alignment.value)
+    }
+
+    internal fun Bundle.getOverlayAttributes(): OverlayAttributes? {
+        val embeddingBounds = getEmbeddingBounds() ?: return null
+        return OverlayAttributes(embeddingBounds)
+    }
+
+    private fun Bundle.getEmbeddingBounds(): EmbeddingBounds? {
+        val embeddingBoundsBundle = getBundle(KEY_EMBEDDING_BOUNDS) ?: return null
+        return EmbeddingBounds(
+            EmbeddingBounds.Alignment(embeddingBoundsBundle.getInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT)),
+            embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_WIDTH),
+            embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_HEIGHT)
+        )
+    }
+
+    /**
+     * Retrieves [EmbeddingBounds.Dimension] value from bundle with given [key].
+     *
+     * See [putDimension] for the data structure of [EmbeddingBounds.Dimension] as bundle
+     */
+    private fun Bundle.getDimension(key: String): Dimension {
+        val dimensionBundle = getBundle(key)!!
+        return when (val type = dimensionBundle.getString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE)) {
+            DIMENSION_TYPE_EXPANDED -> DIMENSION_EXPANDED
+            DIMENSION_TYPE_HINGE -> DIMENSION_HINGE
+            DIMENSION_TYPE_RATIO ->
+                Dimension.ratio(dimensionBundle.getFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
+            DIMENSION_TYPE_PIXEL ->
+                Dimension.pixel(dimensionBundle.getInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
+            else -> throw IllegalArgumentException("Illegal type $type")
+        }
+    }
+
+    /**
+     * Puts [EmbeddingBounds.Dimension] information into bundle with a given [key].
+     *
+     * [EmbeddingBounds.Dimension] is encoded as a [Bundle] with following data structure:
+     * - [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]: A `string` type. Must be one of:
+     *     - [DIMENSION_TYPE_EXPANDED]
+     *     - [DIMENSION_TYPE_HINGE]
+     *     - [DIMENSION_TYPE_RATIO]
+     *     - [DIMENSION_TYPE_PIXEL]
+     * - [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]: Only specified for [DIMENSION_TYPE_RATIO] and
+     *   [DIMENSION_TYPE_PIXEL]. [DIMENSION_TYPE_RATIO] requires a [Float], while
+     *   [DIMENSION_TYPE_PIXEL] requires a [Int].
+     */
+    private fun Bundle.putDimension(key: String, dimension: Dimension) {
+        putBundle(
+            key,
+            Bundle().apply {
+                when (dimension) {
+                    DIMENSION_EXPANDED -> {
+                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_EXPANDED)
+                    }
+                    DIMENSION_HINGE -> {
+                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_HINGE)
+                    }
+                    is Dimension.Ratio -> {
+                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_RATIO)
+                        putFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
+                    }
+                    is Dimension.Pixel -> {
+                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_PIXEL)
+                        putInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
+                    }
+                }
+            }
+        )
+    }
+
+    @RequiresWindowSdkExtension(5)
+    internal fun setActivityStackToken(options: Bundle, activityStackToken: Token) {
+        options.putBundle(KEY_ACTIVITY_STACK_TOKEN, activityStackToken.toBundle())
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
index 24b369a..392dd6c 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -17,15 +17,16 @@
 
 import android.app.Activity
 import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.embedding.ActivityStack.Token
 
 /**
  * A container that holds a stack of activities, overlapping and bound to the same rectangle on the
  * screen.
  */
 class ActivityStack
-@RestrictTo(LIBRARY_GROUP)
-constructor(
+internal constructor(
     /**
      * The [Activity] list in this application's process that belongs to this [ActivityStack].
      *
@@ -42,8 +43,29 @@
      * `false`.
      */
     val isEmpty: Boolean,
+    /** A token uniquely identifying this `ActivityStack`. */
+    private val token: Token?,
 ) {
 
+    /**
+     * Creates ActivityStack ONLY for testing.
+     *
+     * @param activitiesInProcess the [Activity] list in this application's process that belongs to
+     *   this [ActivityStack].
+     * @param isEmpty whether there is no [Activity] running in this [ActivityStack].
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    constructor(
+        activitiesInProcess: List<Activity>,
+        isEmpty: Boolean
+    ) : this(activitiesInProcess, isEmpty, token = null)
+
+    @RequiresWindowSdkExtension(5)
+    internal fun getToken(): Token = let {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+        token!!
+    }
+
     /** Whether this [ActivityStack] contains the [activity]. */
     operator fun contains(activity: Activity): Boolean {
         return activitiesInProcess.contains(activity)
@@ -55,6 +77,7 @@
 
         if (activitiesInProcess != other.activitiesInProcess) return false
         if (isEmpty != other.isEmpty) return false
+        if (token != other.token) return false
 
         return true
     }
@@ -62,9 +85,14 @@
     override fun hashCode(): Int {
         var result = activitiesInProcess.hashCode()
         result = 31 * result + isEmpty.hashCode()
+        result = 31 * result + token.hashCode()
         return result
     }
 
     override fun toString(): String =
-        "ActivityStack{" + "activitiesInProcess=$activitiesInProcess" + ", isEmpty=$isEmpty" + "}"
+        "ActivityStack{" +
+            "activitiesInProcess=$activitiesInProcess" +
+            ", isEmpty=$isEmpty" +
+            ", token=$token" +
+            "}"
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
new file mode 100644
index 0000000..c63928f
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.app.Activity
+import android.graphics.Rect
+import android.util.ArrayMap
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo
+import androidx.window.reflection.Consumer2
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/** Manages and dispatches update of [EmbeddedActivityWindowInfo]. */
+@RequiresWindowSdkExtension(6)
+internal open class ActivityWindowInfoCallbackController(
+    private val embeddingExtension: ActivityEmbeddingComponent,
+) {
+    private val globalLock = ReentrantLock()
+
+    @GuardedBy("globalLock") private val extensionsCallback: Consumer<ExtensionsActivityWindowInfo>
+
+    @VisibleForTesting
+    @GuardedBy("globalLock")
+    internal var callbacks:
+        MutableMap<JetpackConsumer<EmbeddedActivityWindowInfo>, CallbackWrapper> =
+        ArrayMap()
+
+    init {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+        extensionsCallback =
+            Consumer2<ExtensionsActivityWindowInfo> { info ->
+                globalLock.withLock {
+                    for (callbackWrapper in callbacks.values) {
+                        callbackWrapper.accept(info)
+                    }
+                }
+            }
+    }
+
+    fun addCallback(activity: Activity, callback: JetpackConsumer<EmbeddedActivityWindowInfo>) {
+        globalLock.withLock {
+            if (callbacks.isEmpty()) {
+                // Register when the first callback is added.
+                embeddingExtension.setEmbeddedActivityWindowInfoCallback(
+                    Runnable::run,
+                    extensionsCallback
+                )
+            }
+
+            val callbackWrapper = CallbackWrapper(activity, callback)
+            callbacks[callback] = callbackWrapper
+            embeddingExtension.getEmbeddedActivityWindowInfo(activity)?.apply {
+                // Trigger with the latest info if the window exists.
+                callbackWrapper.accept(this)
+            }
+        }
+    }
+
+    fun removeCallback(callback: JetpackConsumer<EmbeddedActivityWindowInfo>) {
+        globalLock.withLock {
+            if (callbacks.remove(callback) == null) {
+                // Early return if the callback is not registered.
+                return
+            }
+            if (callbacks.isEmpty()) {
+                // Unregister when the last callback is removed.
+                embeddingExtension.clearEmbeddedActivityWindowInfoCallback()
+            }
+        }
+    }
+
+    /** Translates from Extensions info to Jetpack info. */
+    @VisibleForTesting
+    internal open fun translate(info: ExtensionsActivityWindowInfo): EmbeddedActivityWindowInfo {
+        val parentHostBounds = Rect(info.taskBounds)
+        val boundsInParentHost = Rect(info.activityStackBounds)
+        // Converting to host container coordinate.
+        boundsInParentHost.offset(-parentHostBounds.left, -parentHostBounds.top)
+        return EmbeddedActivityWindowInfo(
+            isEmbedded = info.isEmbedded,
+            parentHostBounds = parentHostBounds,
+            boundsInParentHost = boundsInParentHost
+        )
+    }
+
+    @VisibleForTesting
+    internal inner class CallbackWrapper(
+        private val activity: Activity,
+        val callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+    ) {
+        var lastReportedInfo: EmbeddedActivityWindowInfo? = null
+
+        fun accept(extensionsActivityWindowInfo: ExtensionsActivityWindowInfo) {
+            val updatedActivity = extensionsActivityWindowInfo.activity
+            if (activity != updatedActivity) {
+                return
+            }
+
+            val newInfo = translate(extensionsActivityWindowInfo)
+            if (shouldReportInfo(newInfo)) {
+                lastReportedInfo = newInfo
+                callback.accept(newInfo)
+            }
+        }
+
+        private fun shouldReportInfo(newInfo: EmbeddedActivityWindowInfo): Boolean =
+            lastReportedInfo?.let {
+                if (it.isEmbedded != newInfo.isEmbedded) {
+                    // Always report if the embedded status changes
+                    return true
+                }
+                if (!newInfo.isEmbedded) {
+                    // Do not report if the activity is not embedded
+                    return false
+                }
+                return it != newInfo
+            } ?: newInfo.isEmbedded // Always report the first available info if it is embedded
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
new file mode 100644
index 0000000..b319fdf
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.window.RequiresWindowSdkExtension
+
+/**
+ * The attributes of the divider layout and behavior.
+ *
+ * @property widthDp the width of the divider.
+ * @property color the color of the divider.
+ * @see SplitAttributes.Builder.setDividerAttributes
+ * @see FixedDividerAttributes
+ * @see DraggableDividerAttributes
+ * @see NO_DIVIDER
+ */
+abstract class DividerAttributes
+private constructor(
+    @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) val widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+    @ColorInt val color: Int = Color.BLACK,
+) {
+    override fun toString(): String =
+        DividerAttributes::class.java.simpleName + "{" + "width=$widthDp, " + "color=$color" + "}"
+
+    /**
+     * The attributes of a fixed divider. A fixed divider is a divider type that draws a static line
+     * between the primary and secondary containers.
+     *
+     * @property widthDp the width of the divider.
+     * @property color the color of the divider.
+     * @see SplitAttributes.Builder.setDividerAttributes
+     */
+    class FixedDividerAttributes
+    @RequiresWindowSdkExtension(6)
+    private constructor(
+        @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+        @ColorInt color: Int = Color.BLACK
+    ) : DividerAttributes(widthDp, color) {
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is FixedDividerAttributes) return false
+            return widthDp == other.widthDp && color == other.color
+        }
+
+        override fun hashCode(): Int = widthDp * 31 + color
+
+        /**
+         * The [FixedDividerAttributes] builder.
+         *
+         * @constructor creates a new [FixedDividerAttributes.Builder]
+         */
+        @RequiresWindowSdkExtension(6)
+        class Builder() {
+            @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
+            private var widthDp = WIDTH_SYSTEM_DEFAULT
+
+            @ColorInt private var color = Color.BLACK
+
+            /**
+             * The [FixedDividerAttributes] builder constructor initialized by an existing
+             * [FixedDividerAttributes].
+             *
+             * @param original the original [FixedDividerAttributes] to initialize the [Builder].
+             */
+            @RequiresWindowSdkExtension(6)
+            constructor(original: FixedDividerAttributes) : this() {
+                widthDp = original.widthDp
+                color = original.color
+            }
+
+            /**
+             * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the system
+             * will choose a default value based on the display size and form factor.
+             *
+             * @throws IllegalArgumentException if the provided value is invalid.
+             */
+            @RequiresWindowSdkExtension(6)
+            fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
+                apply {
+                    validateWidth(widthDp)
+                    this.widthDp = widthDp
+                }
+
+            /**
+             * Sets the color of the divider. If not set, the default color [Color.BLACK] is used.
+             *
+             * @throws IllegalArgumentException if the provided value is invalid.
+             */
+            @RequiresWindowSdkExtension(6)
+            fun setColor(@ColorInt color: Int): Builder = apply {
+                validateColor(color)
+                this.color = color
+            }
+
+            /** Builds a [FixedDividerAttributes] instance. */
+            @RequiresWindowSdkExtension(6)
+            fun build(): FixedDividerAttributes {
+                return FixedDividerAttributes(widthDp = widthDp, color = color)
+            }
+        }
+    }
+
+    /**
+     * The attributes of a draggable divider. A draggable divider draws a line between the primary
+     * and secondary containers with a drag handle that the user can drag and resize the containers.
+     *
+     * While dragging, the content of the activity is temporarily covered by a solid color veil,
+     * where the color is determined by the window background color of the activity. Apps may use
+     * [android.app.Activity.getWindow] and [android.view.Window.setBackgroundDrawable] to configure
+     * the veil colors.
+     *
+     * @property widthDp the width of the divider.
+     * @property color the color of the divider.
+     * @property dragRange the range that a divider is allowed to be dragged. When the user drags
+     *   the divider beyond this range, the system will choose to either fully expand the container
+     *   or move the divider back into the range.
+     * @property isDraggingToFullscreenAllowed if {@code true}, the user is allowed to drag beyond
+     *   the specified range temporarily, and when dragging is finished, the system will choose to
+     *   either fully expand the larger container or move the divider back to the range limit.
+     *   Default to {@code false}.
+     * @see SplitAttributes.Builder.setDividerAttributes
+     */
+    class DraggableDividerAttributes
+    @RequiresWindowSdkExtension(6)
+    private constructor(
+        @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
+        @ColorInt color: Int = Color.BLACK,
+        val dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT,
+        val isDraggingToFullscreenAllowed: Boolean = false,
+    ) : DividerAttributes(widthDp, color) {
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is DraggableDividerAttributes) return false
+            return widthDp == other.widthDp &&
+                color == other.color &&
+                dragRange == other.dragRange &&
+                isDraggingToFullscreenAllowed == other.isDraggingToFullscreenAllowed
+        }
+
+        override fun hashCode(): Int =
+            (((widthDp * 31 + color) * 31 + dragRange.hashCode()) * 31 +
+                isDraggingToFullscreenAllowed.hashCode())
+
+        override fun toString(): String =
+            DraggableDividerAttributes::class.java.simpleName +
+                "{" +
+                "width=$widthDp, " +
+                "color=$color, " +
+                "primaryContainerDragRange=$dragRange, " +
+                "isDraggingToFullscreenAllowed=$isDraggingToFullscreenAllowed" +
+                "}"
+
+        /**
+         * The [DraggableDividerAttributes] builder.
+         *
+         * @constructor creates a new [DraggableDividerAttributes.Builder]
+         */
+        @RequiresWindowSdkExtension(6)
+        class Builder() {
+            @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
+            private var widthDp = WIDTH_SYSTEM_DEFAULT
+
+            @ColorInt private var color = Color.BLACK
+
+            private var dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT
+
+            private var isDraggingToFullscreenAllowed: Boolean = false
+
+            /**
+             * The [DraggableDividerAttributes] builder constructor initialized by an existing
+             * [DraggableDividerAttributes].
+             *
+             * @param original the original [DraggableDividerAttributes] to initialize the [Builder]
+             */
+            @RequiresWindowSdkExtension(6)
+            constructor(original: DraggableDividerAttributes) : this() {
+                widthDp = original.widthDp
+                dragRange = original.dragRange
+                color = original.color
+                isDraggingToFullscreenAllowed = original.isDraggingToFullscreenAllowed
+            }
+
+            /**
+             * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the system
+             * will choose a default value based on the display size and form factor.
+             *
+             * @throws IllegalArgumentException if the provided value is invalid.
+             */
+            @RequiresWindowSdkExtension(6)
+            fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
+                apply {
+                    validateWidth(widthDp)
+                    this.widthDp = widthDp
+                }
+
+            /**
+             * Sets the color of the divider. If not set, the default color [Color.BLACK] is used.
+             *
+             * @throws IllegalArgumentException if the provided value is invalid.
+             */
+            @RequiresWindowSdkExtension(6)
+            fun setColor(@ColorInt color: Int): Builder = apply {
+                validateColor(color)
+                this.color = color
+            }
+
+            /**
+             * Sets the drag range of the divider in terms of the split ratio of the primary
+             * container. It defaults to [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], which means the
+             * system will choose a default value based on the display size and form factor.
+             *
+             * When the user drags the divider beyond this range, the system will choose to either
+             * fully expand the container or move the divider back into the range.
+             *
+             * @param dragRange the [DragRange] for the draggable divider.
+             */
+            @RequiresWindowSdkExtension(6)
+            fun setDragRange(dragRange: DragRange): Builder = apply { this.dragRange = dragRange }
+
+            /**
+             * Sets whether dragging to full screen is allowed.
+             *
+             * If `true`, the user is allowed to drag beyond the specified range temporarily. When
+             * dragging is finished, if the dragging position is below the
+             * [DragRange.SplitRatioDragRange.minRatio] or the default min ratio in
+             * [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], the system will choose to either fully expand
+             * the secondary container or move the divider back to the range limit; if the dragging
+             * position is above the [DragRange.SplitRatioDragRange.maxRatio] or the default max
+             * ratio in [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], the system will choose to either
+             * fully expand the primary container or move the divider back to the range limit.
+             *
+             * When the primary container is fully expanded, the secondary container is dismissed.
+             * When the secondary container is fully expanded, the primary container is hidden
+             * behind the secondary container, and the drag handle is displayed on the edge to allow
+             * the user to drag and bring back the primary container.
+             *
+             * Default to `false`.
+             *
+             * This is only supported on devices with Window SDK extensions version 7 and above. For
+             * devices with Window SDK extensions below version 7, dragging to fullscreen is always
+             * disabled.
+             */
+            @RequiresWindowSdkExtension(7)
+            fun setDraggingToFullscreenAllowed(allowed: Boolean): Builder = apply {
+                this.isDraggingToFullscreenAllowed = allowed
+            }
+
+            /** Builds a [DividerAttributes] instance. */
+            @RequiresWindowSdkExtension(6)
+            fun build(): DraggableDividerAttributes =
+                DraggableDividerAttributes(
+                    widthDp = widthDp,
+                    color = color,
+                    dragRange = dragRange,
+                    isDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed,
+                )
+        }
+    }
+
+    /**
+     * Describes the range that the user is allowed to drag the draggable divider.
+     *
+     * @see SplitRatioDragRange
+     * @see DRAG_RANGE_SYSTEM_DEFAULT
+     */
+    abstract class DragRange private constructor() {
+        /**
+         * A drag range represented as an interval of the primary container's split ratios.
+         *
+         * @constructor constructs a new [SplitRatioDragRange]
+         * @property minRatio the minimum split ratio of the primary container that the user is
+         *   allowed to drag to. When the divider is dragged beyond this ratio, the system will
+         *   choose to either fully expand the secondary container, or move the divider back to this
+         *   ratio.
+         * @property maxRatio the maximum split ratio of the primary container that the user is
+         *   allowed to drag to. When the divider is dragged beyond this ratio, the system will
+         *   choose to either fully expand the primary container, or move the divider back to this
+         *   ratio.
+         * @throws IllegalArgumentException if the provided values are invalid.
+         */
+        class SplitRatioDragRange(
+            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+            val minRatio: Float,
+            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+            val maxRatio: Float,
+        ) : DragRange() {
+            init {
+                if (minRatio <= 0.0 || minRatio >= 1.0) {
+                    throw IllegalArgumentException("minRatio must be in the interval (0.0, 1.0)")
+                }
+                if (maxRatio <= 0.0 || maxRatio >= 1.0) {
+                    throw IllegalArgumentException("maxRatio must be in the interval (0.0, 1.0)")
+                }
+                if (minRatio > maxRatio) {
+                    throw IllegalArgumentException(
+                        "minRatio must be less than or equal to maxRatio"
+                    )
+                }
+            }
+
+            override fun toString(): String = "SplitRatioDragRange[$minRatio, $maxRatio]"
+
+            override fun equals(other: Any?): Boolean {
+                if (this === other) return true
+                if (other !is SplitRatioDragRange) return false
+                return minRatio == other.minRatio && maxRatio == other.maxRatio
+            }
+
+            override fun hashCode(): Int = minRatio.hashCode() * 31 + maxRatio.hashCode()
+        }
+
+        companion object {
+            /**
+             * A special value to indicate that the system will choose default values based on the
+             * display size and form factor.
+             *
+             * @see DraggableDividerAttributes.dragRange
+             */
+            @JvmField
+            val DRAG_RANGE_SYSTEM_DEFAULT =
+                object : DragRange() {
+                    override fun toString(): String = "DRAG_RANGE_SYSTEM_DEFAULT"
+                }
+        }
+    }
+
+    companion object {
+        /**
+         * A special value to indicate that the system will choose a default value based on the
+         * display size and form factor.
+         *
+         * @see DividerAttributes.widthDp
+         */
+        const val WIDTH_SYSTEM_DEFAULT: Int = -1
+
+        /** Indicates that no divider is requested. */
+        @JvmField
+        val NO_DIVIDER =
+            object : DividerAttributes() {
+                override fun toString(): String = "NO_DIVIDER"
+            }
+
+        private fun validateWidth(widthDp: Int) = run {
+            require(widthDp == WIDTH_SYSTEM_DEFAULT || widthDp >= 0) {
+                "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT. Got: $widthDp"
+            }
+        }
+
+        private fun validateColor(@ColorInt color: Int) = run {
+            require(color.alpha() == 255) {
+                "Divider color must be opaque. Got: ${Integer.toHexString(color)}"
+            }
+        }
+
+        /**
+         * Returns the alpha value of the color. This is the same as [Color.alpha] and is used to
+         * avoid test-time dependency.
+         */
+        private fun Int.alpha() = this ushr 24
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
new file mode 100644
index 0000000..f2a3fe6
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Rect
+
+/**
+ * Describes the embedded window related info of an activity.
+ *
+ * When the activity is embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will
+ * be invoked when any fields of [EmbeddedActivityWindowInfo] is changed. When the activity is not
+ * embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will not be triggered
+ * unless the activity is becoming non-embedded from embedded, in which case [isEmbedded] will be
+ * `false`.
+ *
+ * @see ActivityEmbeddingController.embeddedActivityWindowInfo
+ */
+class EmbeddedActivityWindowInfo
+internal constructor(
+    /**
+     * Whether this activity is embedded and its presentation may be customized by the host process
+     * of the task it is associated with.
+     */
+    val isEmbedded: Boolean,
+    /**
+     * The bounds of the host container in display coordinate space, which should be the Task bounds
+     * for regular embedding use case, or if the activity is not embedded.
+     */
+    val parentHostBounds: Rect,
+    /**
+     * The relative bounds of the embedded [ActivityStack] in the host container coordinate space.
+     * It has the same size as [parentHostBounds] if the activity is not embedded.
+     */
+    val boundsInParentHost: Rect,
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is EmbeddedActivityWindowInfo) return false
+
+        if (isEmbedded != other.isEmbedded) return false
+        if (parentHostBounds != other.parentHostBounds) return false
+        if (boundsInParentHost != other.boundsInParentHost) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = isEmbedded.hashCode()
+        result = 31 * result + parentHostBounds.hashCode()
+        result = 31 * result + boundsInParentHost.hashCode()
+        return result
+    }
+
+    override fun toString(): String =
+        "EmbeddedActivityWindowInfo{" +
+            "isEmbedded=$isEmbedded" +
+            ", parentHostBounds=$parentHostBounds" +
+            ", boundsInParentHost=$boundsInParentHost" +
+            "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
index 0191f1f..f9c9439 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -20,12 +20,23 @@
 import android.app.Activity
 import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Binder
 import android.util.LayoutDirection
+import android.util.Log
 import android.util.Pair as AndroidPair
-import android.view.WindowMetrics
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowSdkExtensions
+import androidx.window.core.Bounds
+import androidx.window.core.ExperimentalWindowApi
 import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.DividerAttributes.DragRange.Companion.DRAG_RANGE_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
+import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_ACTIVITY_STACK
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
@@ -38,7 +49,13 @@
 import androidx.window.embedding.SplitAttributes.SplitType.Companion.ratio
 import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
 import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
+import androidx.window.extensions.embedding.AnimationParams as OEMEmbeddingAnimationParams
+import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
+import androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT
 import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
+import androidx.window.extensions.embedding.ParentContainerInfo as OEMParentContainerInfo
 import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
 import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
@@ -49,71 +66,156 @@
 import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
 import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
 import androidx.window.extensions.embedding.SplitPairRule.FINISH_NEVER
+import androidx.window.extensions.embedding.SplitPinRule as OEMSplitPinRule
+import androidx.window.extensions.embedding.SplitPinRule.Builder as SplitPinRuleBuilder
 import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
 import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
+import androidx.window.extensions.embedding.WindowAttributes as OEMWindowAttributes
+import androidx.window.extensions.embedding.WindowAttributes
 import androidx.window.layout.WindowMetricsCalculator
 import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
+import androidx.window.layout.util.DensityCompatHelper
 import androidx.window.reflection.JFunction2
 import androidx.window.reflection.Predicate2
 
 /** Adapter class that translates data classes between Extension and Jetpack interfaces. */
 internal class EmbeddingAdapter(private val predicateAdapter: PredicateAdapter) {
-    private val vendorApiLevel
+    private val extensionVersion
         get() = WindowSdkExtensions.getInstance().extensionVersion
 
     private val api1Impl = VendorApiLevel1Impl(predicateAdapter)
     private val api2Impl = VendorApiLevel2Impl()
+    private val api3Impl = VendorApiLevel3Impl()
+    @OptIn(ExperimentalWindowApi::class) var embeddingConfiguration: EmbeddingConfiguration? = null
 
     fun translate(splitInfoList: List<OEMSplitInfo>): List<SplitInfo> {
         return splitInfoList.map(this::translate)
     }
 
-    @Suppress("DEPRECATION")
     private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
-        return when (vendorApiLevel) {
+        return when (extensionVersion) {
             1 -> api1Impl.translateCompat(splitInfo)
             2 -> api2Impl.translateCompat(splitInfo)
-            else -> {
-                val primaryActivityStack = splitInfo.primaryActivityStack
-                val secondaryActivityStack = splitInfo.secondaryActivityStack
+            in 3..4 -> api3Impl.translateCompat(splitInfo)
+            else ->
                 SplitInfo(
-                    ActivityStack(
-                        primaryActivityStack.activities,
-                        primaryActivityStack.isEmpty,
-                    ),
-                    ActivityStack(
-                        secondaryActivityStack.activities,
-                        secondaryActivityStack.isEmpty,
-                    ),
+                    translate(splitInfo.primaryActivityStack),
+                    translate(splitInfo.secondaryActivityStack),
                     translate(splitInfo.splitAttributes),
-                    splitInfo.token,
+                    splitInfo.splitInfoToken,
                 )
-            }
         }
     }
 
-    internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes =
-        SplitAttributes.Builder()
-            .setSplitType(
-                when (val splitType = splitAttributes.splitType) {
-                    is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
-                    is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
-                    is RatioSplitType -> ratio(splitType.ratio)
-                    else -> throw IllegalArgumentException("Unknown split type: $splitType")
-                }
+    internal fun translate(activityStack: OEMActivityStack): ActivityStack =
+        when (extensionVersion) {
+            in 1..4 -> api1Impl.translateCompat(activityStack)
+            else ->
+                ActivityStack(
+                    activityStack.activities,
+                    activityStack.isEmpty,
+                    activityStack.activityStackToken,
+                )
+        }
+
+    internal fun translate(activityStacks: List<OEMActivityStack>): List<ActivityStack> =
+        activityStacks.map(this::translate)
+
+    @Suppress("DEPRECATION") // To compat with device with extension versions 5 and 6.
+    internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes {
+        val builder =
+            SplitAttributes.Builder()
+                .setSplitType(
+                    when (val splitType = splitAttributes.splitType) {
+                        is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
+                        is OEMSplitType.ExpandContainersSplitType -> SPLIT_TYPE_EXPAND
+                        is RatioSplitType -> ratio(splitType.ratio)
+                        else -> throw IllegalArgumentException("Unknown split type: $splitType")
+                    }
+                )
+                .setLayoutDirection(
+                    when (val layoutDirection = splitAttributes.layoutDirection) {
+                        OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
+                        OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
+                        OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
+                        OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
+                        OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unknown layout direction: $layoutDirection"
+                            )
+                    }
+                )
+        if (extensionVersion in 5..6) {
+            val animationParams =
+                EmbeddingAnimationParams.Builder()
+                    .setAnimationBackground(
+                        translateToJetpackAnimationBackground(splitAttributes.animationBackground)
+                    )
+                    .build()
+            builder.setAnimationParams(animationParams)
+        }
+        if (extensionVersion >= 7) {
+            val animationParams =
+                EmbeddingAnimationParams.Builder()
+                    .setAnimationBackground(
+                        translateToJetpackAnimationBackground(
+                            splitAttributes.animationParams.animationBackground
+                        )
+                    )
+                    .setOpenAnimation(
+                        translateToJetpackAnimationSpec(
+                            splitAttributes.animationParams.openAnimationResId
+                        )
+                    )
+                    .setCloseAnimation(
+                        translateToJetpackAnimationSpec(
+                            splitAttributes.animationParams.closeAnimationResId
+                        )
+                    )
+                    .setChangeAnimation(
+                        translateToJetpackAnimationSpec(
+                            splitAttributes.animationParams.changeAnimationResId
+                        )
+                    )
+                    .build()
+            builder.setAnimationParams(animationParams)
+        }
+        if (extensionVersion >= 6) {
+            builder.setDividerAttributes(
+                translateToJetpackDividerAttributes(splitAttributes.dividerAttributes)
             )
-            .setLayoutDirection(
-                when (val layoutDirection = splitAttributes.layoutDirection) {
-                    OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
-                    OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
-                    OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
-                    OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
-                    OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
-                    else ->
-                        throw IllegalArgumentException("Unknown layout direction: $layoutDirection")
-                }
+        }
+        return builder.build()
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    @OptIn(ExperimentalWindowApi::class)
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    internal fun translate(
+        parentContainerInfo: OEMParentContainerInfo,
+    ): ParentContainerInfo {
+        val configuration = parentContainerInfo.configuration
+        val density =
+            DensityCompatHelper.getInstance()
+                .density(parentContainerInfo.configuration, parentContainerInfo.windowMetrics)
+        val windowMetrics =
+            WindowMetricsCalculator.translateWindowMetrics(
+                parentContainerInfo.windowMetrics,
+                density
             )
-            .build()
+
+        return ParentContainerInfo(
+            Bounds(windowMetrics.bounds),
+            ExtensionsWindowLayoutInfoAdapter.translate(
+                windowMetrics,
+                parentContainerInfo.windowLayoutInfo
+            ),
+            windowMetrics.getWindowInsets(),
+            configuration,
+            density
+        )
+    }
 
     fun translateSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -123,32 +225,35 @@
         }
 
     @SuppressLint("NewApi")
-    fun translate(params: OEMSplitAttributesCalculatorParams): SplitAttributesCalculatorParams =
-        let {
-            val taskWindowMetrics = params.parentWindowMetrics
-            val taskConfiguration = params.parentConfiguration
-            val windowLayoutInfo = params.parentWindowLayoutInfo
-            val defaultSplitAttributes = params.defaultSplitAttributes
-            val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
-            val splitRuleTag = params.splitRuleTag
-            val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
-
-            SplitAttributesCalculatorParams(
-                windowMetrics,
-                taskConfiguration,
-                ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
-                translate(defaultSplitAttributes),
-                areDefaultConstraintsSatisfied,
-                splitRuleTag,
-            )
-        }
+    fun translate(
+        params: OEMSplitAttributesCalculatorParams,
+    ): SplitAttributesCalculatorParams = let {
+        val taskWindowMetrics = params.parentWindowMetrics
+        val taskConfiguration = params.parentConfiguration
+        val windowLayoutInfo = params.parentWindowLayoutInfo
+        val defaultSplitAttributes = params.defaultSplitAttributes
+        val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
+        val splitRuleTag = params.splitRuleTag
+        val density =
+            DensityCompatHelper.getInstance().density(taskConfiguration, taskWindowMetrics)
+        val windowMetrics =
+            WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics, density)
+        SplitAttributesCalculatorParams(
+            windowMetrics,
+            taskConfiguration,
+            ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
+            translate(defaultSplitAttributes),
+            areDefaultConstraintsSatisfied,
+            splitRuleTag,
+        )
+    }
 
     private fun translateSplitPairRule(
         context: Context,
         rule: SplitPairRule,
         predicateClass: Class<*>
     ): OEMSplitPairRule {
-        if (vendorApiLevel < 2) {
+        if (extensionVersion < 2) {
             return api1Impl.translateSplitPairRuleCompat(context, rule, predicateClass)
         } else {
             val activitiesPairPredicate =
@@ -167,7 +272,7 @@
                     }
                 }
             val windowMetricsPredicate =
-                Predicate2<WindowMetrics> { windowMetrics ->
+                Predicate2<AndroidWindowMetrics> { windowMetrics ->
                     rule.checkParentMetrics(context, windowMetrics)
                 }
             val tag = rule.tag
@@ -195,30 +300,103 @@
         }
     }
 
+    @OptIn(ExperimentalWindowApi::class)
+    fun translateSplitPinRule(context: Context, splitPinRule: SplitPinRule): OEMSplitPinRule {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+        val windowMetricsPredicate =
+            Predicate2<AndroidWindowMetrics> { windowMetrics ->
+                splitPinRule.checkParentMetrics(context, windowMetrics)
+            }
+        val builder =
+            SplitPinRuleBuilder(
+                translateSplitAttributes(splitPinRule.defaultSplitAttributes),
+                windowMetricsPredicate
+            )
+        builder.setSticky(splitPinRule.isSticky)
+        val tag = splitPinRule.tag
+        if (tag != null) {
+            builder.setTag(tag)
+        }
+        return builder.build()
+    }
+
+    @OptIn(ExperimentalWindowApi::class)
+    @Suppress("DEPRECATION") // To compat with device with extension versions 5 and 6.
     fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
-        require(vendorApiLevel >= 2)
+        require(extensionVersion >= 2)
         // To workaround the "unused" error in ktlint. It is necessary to translate SplitAttributes
         // from WM Jetpack version to WM extension version.
-        return androidx.window.extensions.embedding.SplitAttributes.Builder()
-            .setSplitType(translateSplitType(splitAttributes.splitType))
-            .setLayoutDirection(
-                when (splitAttributes.layoutDirection) {
-                    LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
-                    LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
-                    RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
-                    TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
-                    BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
-                    else ->
-                        throw IllegalArgumentException(
-                            "Unsupported layoutDirection:" + "$splitAttributes.layoutDirection"
-                        )
-                }
+        val builder =
+            OEMSplitAttributes.Builder()
+                .setSplitType(translateSplitType(splitAttributes.splitType))
+                .setLayoutDirection(
+                    when (splitAttributes.layoutDirection) {
+                        LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
+                        LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
+                        RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
+                        TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+                        BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unsupported layoutDirection:" + "$splitAttributes.layoutDirection"
+                            )
+                    }
+                )
+        if (extensionVersion >= 5) {
+            builder.setWindowAttributes(translateWindowAttributes())
+        }
+        if (extensionVersion in 5..6) {
+            builder.setAnimationBackground(
+                translateToOemAnimationBackground(
+                    splitAttributes.animationParams.animationBackground
+                )
             )
-            .build()
+        }
+        if (extensionVersion >= 7) {
+            val animationParams =
+                OEMEmbeddingAnimationParams.Builder()
+                    .setAnimationBackground(
+                        translateToOemAnimationBackground(
+                            splitAttributes.animationParams.animationBackground
+                        )
+                    )
+                    .setOpenAnimationResId(
+                        translateToOemAnimationResId(splitAttributes.animationParams.openAnimation)
+                    )
+                    .setCloseAnimationResId(
+                        translateToOemAnimationResId(splitAttributes.animationParams.closeAnimation)
+                    )
+                    .setChangeAnimationResId(
+                        translateToOemAnimationResId(
+                            splitAttributes.animationParams.changeAnimation
+                        )
+                    )
+                    .build()
+            builder.setAnimationParams(animationParams)
+        }
+        if (extensionVersion >= 6) {
+            builder.setDividerAttributes(
+                translateToOemDividerAttributes(splitAttributes.dividerAttributes)
+            )
+        }
+        return builder.build()
+    }
+
+    /** Translates [embeddingConfiguration] from adapter to [WindowAttributes]. */
+    @OptIn(ExperimentalWindowApi::class)
+    internal fun translateWindowAttributes(): OEMWindowAttributes = let {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+
+        OEMWindowAttributes(
+            when (embeddingConfiguration?.dimAreaBehavior) {
+                ON_ACTIVITY_STACK -> OEMWindowAttributes.DIM_AREA_ON_ACTIVITY_STACK
+                else -> OEMWindowAttributes.DIM_AREA_ON_TASK
+            }
+        )
     }
 
     private fun translateSplitType(splitType: SplitType): OEMSplitType {
-        require(vendorApiLevel >= 2)
+        require(extensionVersion >= 2)
         return when (splitType) {
             SPLIT_TYPE_HINGE -> OEMSplitType.HingeSplitType(translateSplitType(SPLIT_TYPE_EQUAL))
             SPLIT_TYPE_EXPAND -> OEMSplitType.ExpandContainersSplitType()
@@ -240,7 +418,7 @@
         rule: SplitPlaceholderRule,
         predicateClass: Class<*>
     ): OEMSplitPlaceholderRule {
-        if (vendorApiLevel < 2) {
+        if (extensionVersion < 2) {
             return api1Impl.translateSplitPlaceholderRuleCompat(context, rule, predicateClass)
         } else {
             val activityPredicate =
@@ -252,7 +430,7 @@
                     rule.filters.any { filter -> filter.matchesIntent(intent) }
                 }
             val windowMetricsPredicate =
-                Predicate2<WindowMetrics> { windowMetrics ->
+                Predicate2<AndroidWindowMetrics> { windowMetrics ->
                     rule.checkParentMetrics(context, windowMetrics)
                 }
             val tag = rule.tag
@@ -289,7 +467,7 @@
         rule: ActivityRule,
         predicateClass: Class<*>
     ): OEMActivityRule {
-        if (vendorApiLevel < 2) {
+        if (extensionVersion < 2) {
             return api1Impl.translateActivityRuleCompat(rule, predicateClass)
         } else {
             val activityPredicate =
@@ -326,29 +504,160 @@
             .toSet()
     }
 
+    @RequiresWindowSdkExtension(5)
+    private fun translateToOemAnimationBackground(
+        animationBackground: EmbeddingAnimationBackground
+    ): OEMEmbeddingAnimationBackground {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+        return if (animationBackground is EmbeddingAnimationBackground.ColorBackground) {
+            OEMEmbeddingAnimationBackground.createColorBackground(animationBackground.color)
+        } else {
+            OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
+        }
+    }
+
+    @RequiresWindowSdkExtension(5)
+    private fun translateToJetpackAnimationBackground(
+        animationBackground: OEMEmbeddingAnimationBackground
+    ): EmbeddingAnimationBackground {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+        return if (animationBackground is OEMEmbeddingAnimationBackground.ColorBackground) {
+            EmbeddingAnimationBackground.createColorBackground(animationBackground.color)
+        } else {
+            EmbeddingAnimationBackground.DEFAULT
+        }
+    }
+
+    @RequiresWindowSdkExtension(7)
+    private fun translateToOemAnimationResId(
+        animationSpec: EmbeddingAnimationParams.AnimationSpec
+    ): Int {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(7)
+        return if (animationSpec == EmbeddingAnimationParams.AnimationSpec.JUMP_CUT) {
+            Resources.ID_NULL
+        } else {
+            OEMEmbeddingAnimationParams.DEFAULT_ANIMATION_RESOURCES_ID
+        }
+    }
+
+    @RequiresWindowSdkExtension(7)
+    private fun translateToJetpackAnimationSpec(
+        animationResId: Int
+    ): EmbeddingAnimationParams.AnimationSpec {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(7)
+        return if (animationResId == Resources.ID_NULL) {
+            EmbeddingAnimationParams.AnimationSpec.JUMP_CUT
+        } else {
+            EmbeddingAnimationParams.AnimationSpec.DEFAULT
+        }
+    }
+
+    @RequiresWindowSdkExtension(6)
+    fun translateToOemDividerAttributes(
+        dividerAttributes: DividerAttributes
+    ): OEMDividerAttributes? {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+        if (dividerAttributes === DividerAttributes.NO_DIVIDER) {
+            return null
+        }
+        val builder =
+            OEMDividerAttributes.Builder(
+                    when (dividerAttributes) {
+                        is FixedDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_FIXED
+                        is DraggableDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unknown divider attributes $dividerAttributes"
+                            )
+                    }
+                )
+                .setDividerColor(dividerAttributes.color)
+                .setWidthDp(dividerAttributes.widthDp)
+        if (dividerAttributes is DraggableDividerAttributes) {
+            if (dividerAttributes.dragRange is SplitRatioDragRange) {
+                builder
+                    .setPrimaryMinRatio(dividerAttributes.dragRange.minRatio)
+                    .setPrimaryMaxRatio(dividerAttributes.dragRange.maxRatio)
+            }
+            if (extensionVersion >= 7) {
+                builder.setDraggingToFullscreenAllowed(
+                    dividerAttributes.isDraggingToFullscreenAllowed
+                )
+            }
+        }
+        return builder.build()
+    }
+
+    @RequiresWindowSdkExtension(6)
+    fun translateToJetpackDividerAttributes(
+        oemDividerAttributes: OEMDividerAttributes?
+    ): DividerAttributes {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
+        if (oemDividerAttributes == null) {
+            return DividerAttributes.NO_DIVIDER
+        }
+        return when (oemDividerAttributes.dividerType) {
+            OEMDividerAttributes.DIVIDER_TYPE_FIXED ->
+                FixedDividerAttributes.Builder()
+                    .setWidthDp(oemDividerAttributes.widthDp)
+                    .setColor(oemDividerAttributes.dividerColor)
+                    .build()
+            OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE ->
+                DraggableDividerAttributes.Builder()
+                    .setWidthDp(oemDividerAttributes.widthDp)
+                    .setColor(oemDividerAttributes.dividerColor)
+                    .setDragRange(
+                        if (
+                            oemDividerAttributes.primaryMinRatio == RATIO_SYSTEM_DEFAULT &&
+                                oemDividerAttributes.primaryMaxRatio == RATIO_SYSTEM_DEFAULT
+                        )
+                            DRAG_RANGE_SYSTEM_DEFAULT
+                        else
+                            SplitRatioDragRange(
+                                oemDividerAttributes.primaryMinRatio,
+                                oemDividerAttributes.primaryMaxRatio,
+                            )
+                    )
+                    .setDraggingToFullscreenAllowed(
+                        extensionVersion >= 7 && oemDividerAttributes.isDraggingToFullscreenAllowed
+                    )
+                    .build()
+            // Default to DividerType.FIXED
+            else -> {
+                Log.w(
+                    TAG,
+                    "Unknown divider type $oemDividerAttributes.dividerType, default" +
+                        " to fixed divider type"
+                )
+                FixedDividerAttributes.Builder()
+                    .setWidthDp(oemDividerAttributes.widthDp)
+                    .setColor(oemDividerAttributes.dividerColor)
+                    .build()
+            }
+        }
+    }
+
+    /** Provides backward compatibility for Window extensions with API level 3 */
+    // Suppress deprecation because this object is to provide backward compatibility.
+    @Suppress("DEPRECATION")
+    private inner class VendorApiLevel3Impl {
+        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
+            SplitInfo(
+                api1Impl.translateCompat(splitInfo.primaryActivityStack),
+                api1Impl.translateCompat(splitInfo.secondaryActivityStack),
+                translate(splitInfo.splitAttributes),
+                splitInfo.token,
+            )
+    }
+
     /** Provides backward compatibility for Window extensions with API level 2 */
     private inner class VendorApiLevel2Impl {
-        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo {
-            val primaryActivityStack = splitInfo.primaryActivityStack
-            val primaryFragment =
-                ActivityStack(
-                    primaryActivityStack.activities,
-                    primaryActivityStack.isEmpty,
-                )
-
-            val secondaryActivityStack = splitInfo.secondaryActivityStack
-            val secondaryFragment =
-                ActivityStack(
-                    secondaryActivityStack.activities,
-                    secondaryActivityStack.isEmpty,
-                )
-            return SplitInfo(
-                primaryFragment,
-                secondaryFragment,
+        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
+            SplitInfo(
+                api1Impl.translateCompat(splitInfo.primaryActivityStack),
+                api1Impl.translateCompat(splitInfo.secondaryActivityStack),
                 translate(splitInfo.splitAttributes),
-                INVALID_SPLIT_INFO_TOKEN,
             )
-        }
     }
 
     /** Provides backward compatibility for [WindowSdkExtensions] version 1 */
@@ -508,35 +817,31 @@
 
         @SuppressLint("ClassVerificationFailure", "NewApi")
         private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
-            predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
+            predicateAdapter.buildPredicate(AndroidWindowMetrics::class) { windowMetrics ->
                 splitRule.checkParentMetrics(context, windowMetrics)
             }
 
         fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo =
             SplitInfo(
-                ActivityStack(
-                    splitInfo.primaryActivityStack.activities,
-                    splitInfo.primaryActivityStack.isEmpty,
-                ),
-                ActivityStack(
-                    splitInfo.secondaryActivityStack.activities,
-                    splitInfo.secondaryActivityStack.isEmpty,
-                ),
+                translateCompat(splitInfo.primaryActivityStack),
+                translateCompat(splitInfo.secondaryActivityStack),
                 getSplitAttributesCompat(splitInfo),
-                INVALID_SPLIT_INFO_TOKEN,
+            )
+
+        fun translateCompat(activityStack: OEMActivityStack): ActivityStack =
+            ActivityStack(
+                activityStack.activities,
+                activityStack.isEmpty,
             )
     }
 
     internal companion object {
+        private val TAG = EmbeddingAdapter::class.simpleName
+
         /**
          * The default token of [SplitInfo], which provides compatibility for device prior to vendor
          * API level 3
          */
         val INVALID_SPLIT_INFO_TOKEN = Binder()
-        /**
-         * The default token of [ActivityStack], which provides compatibility for device prior to
-         * vendor API level 3
-         */
-        val INVALID_ACTIVITY_STACK_TOKEN = Binder()
     }
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
new file mode 100644
index 0000000..64a354f
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import androidx.annotation.IntRange
+
+/**
+ * Background to be used for window transition animations for embedding activities if the animation
+ * requires a background.
+ *
+ * @see EmbeddingAnimationParams.animationBackground
+ */
+abstract class EmbeddingAnimationBackground private constructor() {
+
+    /**
+     * An {@link EmbeddingAnimationBackground} to specify of using a developer-defined color as the
+     * animation background. Only opaque background is supported.
+     *
+     * @see EmbeddingAnimationBackground.createColorBackground
+     */
+    class ColorBackground
+    internal constructor(
+        /** [ColorInt] to represent the color to use as the background color. */
+        @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong()) @ColorInt val color: Int
+    ) : EmbeddingAnimationBackground() {
+
+        init {
+            require(Color.alpha(color) == 255) { "Background color must be opaque" }
+        }
+
+        override fun toString() = "ColorBackground{color:${Integer.toHexString(color)}}"
+
+        override fun equals(other: Any?): Boolean {
+            if (other === this) return true
+            if (other !is ColorBackground) return false
+            return color == other.color
+        }
+
+        override fun hashCode() = color.hashCode()
+    }
+
+    /** @see EmbeddingAnimationBackground.DEFAULT */
+    private class DefaultBackground : EmbeddingAnimationBackground() {
+
+        override fun toString() = "DefaultBackground"
+    }
+
+    /** Methods that create various [EmbeddingAnimationBackground]. */
+    companion object {
+
+        /**
+         * Creates a [ColorBackground] to represent the given [color].
+         *
+         * Only opaque color is supported.
+         *
+         * @param color [ColorInt] of an opaque color.
+         * @return the [ColorBackground] representing the [color].
+         * @throws IllegalArgumentException if the [color] is not opaque.
+         * @see [DEFAULT] for the default value, which means to use the current theme window
+         *   background color.
+         */
+        @JvmStatic
+        fun createColorBackground(
+            @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong()) @ColorInt color: Int
+        ): ColorBackground = ColorBackground(color)
+
+        /**
+         * The special [EmbeddingAnimationBackground] to represent the default value, which means to
+         * use the current theme window background color.
+         */
+        @JvmField val DEFAULT: EmbeddingAnimationBackground = DefaultBackground()
+
+        /** Returns an [EmbeddingAnimationBackground] with the given [color] */
+        internal fun buildFromValue(@ColorInt color: Int): EmbeddingAnimationBackground {
+            return if (Color.alpha(color) != 255) {
+                // Treat any non-opaque color as the default.
+                DEFAULT
+            } else {
+                createColorBackground(color)
+            }
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt
new file mode 100644
index 0000000..7473e49
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationParams.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion.DEFAULT
+
+/**
+ * Parameters to be used for window transition animations for embedding activities.
+ *
+ * @property animationBackground the animation background to use during the animation of the split
+ *   involving this `EmbeddingAnimationParams` object if the animation requires a background. The
+ *   default is to use the current theme window background color.
+ * @property openAnimation the animation spec to use for open transitions (when starting/entering an
+ *   activity or when an activity moves to front).
+ * @property closeAnimation the animation spec to use for close transitions (when finishing/exiting
+ *   an activity or when an activity moves to back).
+ * @property changeAnimation the animation spec to use for change transitions (when an activity
+ *   resizes or moves).
+ * @see SplitAttributes.animationParams
+ * @see EmbeddingAnimationBackground
+ * @see EmbeddingAnimationBackground.createColorBackground
+ * @see EmbeddingAnimationBackground.DEFAULT
+ */
+class EmbeddingAnimationParams
+@JvmOverloads
+constructor(
+    val animationBackground: EmbeddingAnimationBackground = EmbeddingAnimationBackground.DEFAULT,
+    val openAnimation: AnimationSpec = DEFAULT,
+    val closeAnimation: AnimationSpec = DEFAULT,
+    val changeAnimation: AnimationSpec = DEFAULT,
+) {
+
+    /** The animation to use when an activity transitions (e.g. open, close, or change). */
+    class AnimationSpec
+    private constructor(
+        /**
+         * The unique integer value for the `splitAnimationSpec`. This can be used as an enum value
+         * when defining `splitAnimationSpec` attributes in XML.
+         */
+        internal val value: Int,
+    ) {
+
+        /**
+         * A string representation of this `AnimationSpec`.
+         *
+         * @return the string representation of the object.
+         */
+        override fun toString(): String =
+            when (value) {
+                0 -> "DEFAULT"
+                1 -> "JUMP_CUT"
+                else -> "Unknown value: $value"
+            }
+
+        /** Properties and methods. */
+        companion object {
+            /** Specifies the default animation defined by the system. */
+            @JvmField val DEFAULT = AnimationSpec(0)
+            /** Specifies an animation with zero duration. */
+            @JvmField val JUMP_CUT = AnimationSpec(1)
+
+            /** Returns `AnimationSpec` with the given integer `value`. */
+            @JvmStatic
+            internal fun getAnimationSpecFromValue(@IntRange(from = 0, to = 1) value: Int) =
+                when (value) {
+                    DEFAULT.value -> DEFAULT
+                    JUMP_CUT.value -> JUMP_CUT
+                    else -> throw IllegalArgumentException("Undefined value:$value")
+                }
+        }
+    }
+
+    /**
+     * Returns a hash code for this `EmbeddingAnimationParams` object.
+     *
+     * @return the hash code for this object.
+     */
+    override fun hashCode(): Int {
+        var result = animationBackground.hashCode()
+        result = result * 31 + openAnimation.hashCode()
+        result = result * 31 + closeAnimation.hashCode()
+        result = result * 31 + changeAnimation.hashCode()
+        return result
+    }
+
+    /**
+     * Determines whether this object has the same animation parameters as the compared object.
+     *
+     * @param other the object to compare to this object.
+     * @return true if the objects have the same animation parameters, false otherwise.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is EmbeddingAnimationParams) return false
+        return animationBackground == other.animationBackground &&
+            openAnimation == other.openAnimation &&
+            closeAnimation == other.closeAnimation &&
+            changeAnimation == other.changeAnimation
+    }
+
+    /**
+     * A string representation of this `EmbeddingAnimationParams` object.
+     *
+     * @return the string representation of the object.
+     */
+    override fun toString(): String =
+        "${EmbeddingAnimationParams::class.java.simpleName}:" +
+            "{animationBackground=$animationBackground, openAnimation=$openAnimation, " +
+            "closeAnimation=$closeAnimation, changeAnimation=$changeAnimation }"
+
+    /** Builder for creating an instance of [EmbeddingAnimationParams]. */
+    class Builder {
+        private var animationBackground = EmbeddingAnimationBackground.DEFAULT
+        private var openAnimation = DEFAULT
+        private var closeAnimation = DEFAULT
+        private var changeAnimation = DEFAULT
+
+        /**
+         * Sets the animation background.
+         *
+         * The default is to use the current theme window background color.
+         *
+         * @param background the animation background.
+         * @return this `Builder`.
+         * @see EmbeddingAnimationBackground
+         */
+        fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder = apply {
+            this.animationBackground = background
+        }
+
+        /**
+         * Sets the open animation.
+         *
+         * The default is to use the system animation.
+         *
+         * @param spec the animation transition spec
+         * @return this `Builder`.
+         */
+        fun setOpenAnimation(spec: AnimationSpec): Builder = apply { this.openAnimation = spec }
+
+        /**
+         * Sets the close animation.
+         *
+         * The default is to use the system animation.
+         *
+         * @param spec the animation transition spec
+         * @return this `Builder`.
+         */
+        fun setCloseAnimation(spec: AnimationSpec): Builder = apply { this.closeAnimation = spec }
+
+        /**
+         * Sets the change (resize or move) animation.
+         *
+         * The default is to use the system animation.
+         *
+         * @param spec the animation spec
+         * @return this `Builder`.
+         */
+        fun setChangeAnimation(spec: AnimationSpec): Builder = apply { this.changeAnimation = spec }
+
+        /**
+         * Builds an `EmbeddingAnimationParams` instance with the attributes specified by the
+         * builder's setters.
+         *
+         * @return the new `EmbeddingAnimationParams` instance.
+         */
+        fun build(): EmbeddingAnimationParams =
+            EmbeddingAnimationParams(
+                animationBackground,
+                openAnimation,
+                closeAnimation,
+                changeAnimation
+            )
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
index 2711813..a66b290 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -17,12 +17,12 @@
 package androidx.window.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
 import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
 import androidx.annotation.RestrictTo
 import androidx.core.util.Consumer
 import androidx.window.RequiresWindowSdkExtension
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import java.util.concurrent.Executor
 
 /**  */
@@ -48,6 +48,11 @@
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
+    @RequiresWindowSdkExtension(5)
+    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
+
+    @RequiresWindowSdkExtension(5) fun unpinTopActivityStack(taskId: Int)
+
     @RequiresWindowSdkExtension(2)
     fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -57,14 +62,53 @@
 
     fun getActivityStack(activity: Activity): ActivityStack?
 
-    @RequiresWindowSdkExtension(3)
-    fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+    @RequiresWindowSdkExtension(5)
+    fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
 
-    @RequiresWindowSdkExtension(3) fun invalidateTopVisibleSplitAttributes()
+    @RequiresWindowSdkExtension(5)
+    fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
+
+    @RequiresWindowSdkExtension(5) fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+    @RequiresWindowSdkExtension(5)
+    fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
+
+    @RequiresWindowSdkExtension(3) fun invalidateVisibleActivityStacks()
 
     @RequiresWindowSdkExtension(3)
     fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
 
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    )
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION) fun clearOverlayAttributesCalculator()
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: Consumer<OverlayInfo>,
+    )
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>)
+
+    @RequiresWindowSdkExtension(6)
+    fun addEmbeddedActivityWindowInfoCallbackForActivity(
+        activity: Activity,
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    )
+
+    @RequiresWindowSdkExtension(6)
+    fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    )
+
     companion object {
 
         private var decorator: (EmbeddingBackend) -> EmbeddingBackend = { it }
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
new file mode 100644
index 0000000..42ef668
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Rect
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.annotation.Px
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.window.core.Bounds
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
+import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
+import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import kotlin.math.min
+
+/**
+ * The bounds of a standalone [ActivityStack].
+ *
+ * It can be either described with `alignment`, `width` and `height` or predefined constant values.
+ * Some important constants are:
+ * - [BOUNDS_EXPANDED]: To indicate the bounds fills the parent window container.
+ * - [BOUNDS_HINGE_TOP]: To indicate the bounds are at the top of the parent window container while
+ *   its bottom follows the hinge position. Refer to [BOUNDS_HINGE_LEFT], [BOUNDS_HINGE_BOTTOM] and
+ *   [BOUNDS_HINGE_RIGHT] for other bounds that follows the hinge position.
+ *
+ * @constructor creates an embedding bounds.
+ * @property alignment The alignment of the bounds relative to parent window container.
+ * @property width The width of the bounds.
+ * @property height The height of the bounds.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class EmbeddingBounds(val alignment: Alignment, val width: Dimension, val height: Dimension) {
+    override fun toString(): String {
+        return "Bounds:{alignment=$alignment, width=$width, height=$height}"
+    }
+
+    override fun hashCode(): Int {
+        var result = alignment.hashCode()
+        result = result * 31 + width.hashCode()
+        result = result * 31 + height.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is EmbeddingBounds) return false
+        return alignment == other.alignment && width == other.width && height == other.height
+    }
+
+    /** Returns `true` if the [width] should fallback to half of parent task width. */
+    internal fun shouldUseFallbackDimensionForWidth(windowLayoutInfo: WindowLayoutInfo): Boolean {
+        if (width != DIMENSION_HINGE) {
+            return false
+        }
+        return !windowLayoutInfo.isVertical() || alignment in listOf(ALIGN_TOP, ALIGN_BOTTOM)
+    }
+
+    /** Returns `true` if the [height] should fallback to half of parent task height. */
+    internal fun shouldUseFallbackDimensionForHeight(windowLayoutInfo: WindowLayoutInfo): Boolean {
+        if (height != DIMENSION_HINGE) {
+            return false
+        }
+        return !windowLayoutInfo.isHorizontal() || alignment in listOf(ALIGN_LEFT, ALIGN_RIGHT)
+    }
+
+    private fun WindowLayoutInfo.isHorizontal(): Boolean {
+        val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
+        return foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+    }
+
+    private fun WindowLayoutInfo.isVertical(): Boolean {
+        val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
+        return foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
+    }
+
+    /**
+     * Returns [FoldingFeature] if it's the only `FoldingFeature` in [WindowLayoutInfo]. Returns
+     * `null`, otherwise.
+     */
+    private fun WindowLayoutInfo.getOnlyFoldingFeatureOrNull(): FoldingFeature? {
+        val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
+        return if (foldingFeatures.size == 1) foldingFeatures[0] else null
+    }
+
+    /** Calculates [width] in pixel with [parentContainerBounds] and [windowLayoutInfo]. */
+    @Px
+    internal fun getWidthInPixel(
+        parentContainerBounds: Bounds,
+        windowLayoutInfo: WindowLayoutInfo
+    ): Int {
+        val taskWidth = parentContainerBounds.width
+        val widthDimension =
+            if (shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
+                ratio(0.5f)
+            } else {
+                width
+            }
+        when (widthDimension) {
+            is Dimension.Ratio -> return widthDimension * taskWidth
+            is Dimension.Pixel -> return min(taskWidth, widthDimension.value)
+            DIMENSION_HINGE -> {
+                // Should be verified by #shouldUseFallbackDimensionForWidth
+                val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
+                return when (alignment) {
+                    ALIGN_LEFT -> {
+                        hingeBounds.left - parentContainerBounds.left
+                    }
+                    ALIGN_RIGHT -> {
+                        parentContainerBounds.right - hingeBounds.right
+                    }
+                    else -> {
+                        throw IllegalStateException(
+                            "Unhandled condition to get height in pixel! " +
+                                "embeddingBounds=$this taskBounds=$parentContainerBounds " +
+                                "windowLayoutInfo=$windowLayoutInfo"
+                        )
+                    }
+                }
+            }
+            else -> throw IllegalArgumentException("Unhandled width dimension=$width")
+        }
+    }
+
+    /** Calculates [height] in pixel with [parentContainerBounds] and [windowLayoutInfo]. */
+    @Px
+    internal fun getHeightInPixel(
+        parentContainerBounds: Bounds,
+        windowLayoutInfo: WindowLayoutInfo
+    ): Int {
+        val taskHeight = parentContainerBounds.height
+        val heightDimension =
+            if (shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {
+                ratio(0.5f)
+            } else {
+                height
+            }
+        when (heightDimension) {
+            is Dimension.Ratio -> return heightDimension * taskHeight
+            is Dimension.Pixel -> return min(taskHeight, heightDimension.value)
+            DIMENSION_HINGE -> {
+                // Should be verified by #shouldUseFallbackDimensionForWidth
+                val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
+                return when (alignment) {
+                    ALIGN_TOP -> {
+                        hingeBounds.top - parentContainerBounds.top
+                    }
+                    ALIGN_BOTTOM -> {
+                        parentContainerBounds.bottom - hingeBounds.bottom
+                    }
+                    else -> {
+                        throw IllegalStateException(
+                            "Unhandled condition to get height in pixel! " +
+                                "embeddingBounds=$this taskBounds=$parentContainerBounds " +
+                                "windowLayoutInfo=$windowLayoutInfo"
+                        )
+                    }
+                }
+            }
+            else -> throw IllegalArgumentException("Unhandled width dimension=$width")
+        }
+    }
+
+    /** The position of the bounds relative to parent window container. */
+    class Alignment internal constructor(@IntRange(from = 0, to = 3) internal val value: Int) {
+
+        init {
+            require(value in 0..3)
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Alignment) return false
+            return value == other.value
+        }
+
+        override fun hashCode(): Int {
+            return value
+        }
+
+        override fun toString(): String =
+            when (value) {
+                0 -> "left"
+                1 -> "top"
+                2 -> "right"
+                3 -> "bottom"
+                else -> "unknown position:$value"
+            }
+
+        companion object {
+
+            /** Specifies that the bounds is at the left of the parent window container. */
+            @JvmField val ALIGN_LEFT = Alignment(0)
+
+            /** Specifies that the bounds is at the top of the parent window container. */
+            @JvmField val ALIGN_TOP = Alignment(1)
+
+            /** Specifies that the bounds is at the right of the parent window container. */
+            @JvmField val ALIGN_RIGHT = Alignment(2)
+
+            /** Specifies that the bounds is at the bottom of the parent window container. */
+            @JvmField val ALIGN_BOTTOM = Alignment(3)
+        }
+    }
+
+    /**
+     * The dimension of the bounds, which can be represented as multiple formats:
+     * - [DIMENSION_EXPANDED]: means the bounds' dimension fills parent window's dimension.
+     * - in [pixel]: To specify the dimension value in pixel.
+     * - in [ratio]: To specify the dimension that relative to the parent window container. For
+     *   example, if [width] has [ratio] value 0.6, it means the bounds' width is 0.6 to the parent
+     *   window container's width.
+     */
+    abstract class Dimension internal constructor(internal val description: String) {
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Dimension) return false
+            return description == other.description
+        }
+
+        override fun hashCode(): Int = description.hashCode()
+
+        override fun toString(): String = description
+
+        /**
+         * The [Dimension] represented in pixel format
+         *
+         * @param value The dimension length in pixel
+         */
+        internal class Pixel(@Px @IntRange(from = 1) internal val value: Int) :
+            Dimension("dimension in pixel:$value") {
+
+            init {
+                require(value >= 1) { "Pixel value must be a positive integer." }
+            }
+
+            internal operator fun compareTo(dimen: Int): Int = value - dimen
+        }
+
+        /**
+         * The [Dimension] represented in ratio format, which means the proportion of the parent
+         * window dimension.
+         *
+         * @param value The ratio in (0.0, 1.0)
+         */
+        internal class Ratio(
+            @FloatRange(from = 0.0, fromInclusive = false, to = 1.0) internal val value: Float
+        ) : Dimension("dimension in ratio:$value") {
+
+            init {
+                require(value > 0.0 && value <= 1.0) { "Ratio must be in range (0.0, 1.0]" }
+            }
+
+            internal operator fun times(dimen: Int): Int = (value * dimen).toInt()
+        }
+
+        companion object {
+
+            /** Represents this dimension follows its parent window dimension. */
+            @JvmField val DIMENSION_EXPANDED: Dimension = Ratio(1.0f)
+
+            /**
+             * Represents this dimension follows the hinge position if the current window and device
+             * state satisfies, or fallbacks to a half of the parent task dimension, otherwise.
+             *
+             * The [DIMENSION_HINGE] works only if:
+             * - The parent container is not in multi-window mode (e.g., split-screen mode or
+             *   picture-in-picture mode)
+             * - The device has a hinge or separating fold reported by
+             *   [androidx.window.layout.FoldingFeature.isSeparating]
+             * - The hinge or separating fold orientation matches [EmbeddingBounds.alignment]:
+             *     - The hinge or fold orientation is vertical, and the position is [POSITION_LEFT]
+             *       or [POSITION_RIGHT]
+             *     - The hinge or fold orientation is horizontal, and the position is [POSITION_TOP]
+             *       or [POSITION_BOTTOM]
+             */
+            @JvmField val DIMENSION_HINGE: Dimension = object : Dimension("hinge") {}
+
+            /**
+             * Creates the dimension in pixel.
+             *
+             * If the dimension length exceeds the parent window dimension, the overlay container
+             * will resize to fit the parent task dimension.
+             *
+             * @param value The dimension length in pixel
+             */
+            @JvmStatic fun pixel(@Px @IntRange(from = 1) value: Int): Dimension = Pixel(value)
+
+            /**
+             * Creates the dimension which takes a proportion of the parent window dimension.
+             *
+             * @param ratio The proportion of the parent window dimension this dimension should take
+             */
+            @JvmStatic
+            fun ratio(
+                @FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = false)
+                ratio: Float
+            ): Dimension = Ratio(ratio)
+        }
+    }
+
+    companion object {
+
+        /** The bounds fills the parent window bounds */
+        @JvmField
+        val BOUNDS_EXPANDED = EmbeddingBounds(ALIGN_TOP, DIMENSION_EXPANDED, DIMENSION_EXPANDED)
+
+        /**
+         * The bounds located on the top of the parent window, and the bounds' bottom side matches
+         * the hinge position.
+         */
+        @JvmField
+        val BOUNDS_HINGE_TOP =
+            EmbeddingBounds(ALIGN_TOP, width = DIMENSION_EXPANDED, height = DIMENSION_HINGE)
+
+        /**
+         * The bounds located on the left of the parent window, and the bounds' right side matches
+         * the hinge position.
+         */
+        @JvmField
+        val BOUNDS_HINGE_LEFT =
+            EmbeddingBounds(ALIGN_LEFT, width = DIMENSION_HINGE, height = DIMENSION_EXPANDED)
+
+        /**
+         * The bounds located on the bottom of the parent window, and the bounds' top side matches
+         * the hinge position.
+         */
+        @JvmField
+        val BOUNDS_HINGE_BOTTOM =
+            EmbeddingBounds(ALIGN_BOTTOM, width = DIMENSION_EXPANDED, height = DIMENSION_HINGE)
+
+        /**
+         * The bounds located on the right of the parent window, and the bounds' left side matches
+         * the hinge position.
+         */
+        @JvmField
+        val BOUNDS_HINGE_RIGHT =
+            EmbeddingBounds(ALIGN_RIGHT, width = DIMENSION_HINGE, height = DIMENSION_EXPANDED)
+
+        /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
+        @VisibleForTesting
+        internal fun translateEmbeddingBounds(
+            embeddingBounds: EmbeddingBounds,
+            parentContainerBounds: Bounds,
+            windowLayoutInfo: WindowLayoutInfo,
+        ): Bounds {
+            if (
+                embeddingBounds.width == DIMENSION_EXPANDED &&
+                    embeddingBounds.height == DIMENSION_EXPANDED
+            ) {
+                // If width and height are expanded, set bounds to empty to follow the parent task
+                // bounds.
+                return Bounds.EMPTY_BOUNDS
+            }
+            // 1. Fallbacks dimensions to ratio(0.5) if they can't follow the hinge with the current
+            //    device and window state.
+            val width =
+                if (embeddingBounds.shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
+                    ratio(0.5f)
+                } else {
+                    embeddingBounds.width
+                }
+            val height =
+                if (embeddingBounds.shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {
+                    ratio(0.5f)
+                } else {
+                    embeddingBounds.height
+                }
+
+            // 2. Computes dimensions to pixel values. If it just matches parent task bounds,
+            // returns
+            //    the empty bounds to declare the bounds follow the parent task bounds.
+            val sanitizedBounds = EmbeddingBounds(embeddingBounds.alignment, width, height)
+            val widthInPixel =
+                sanitizedBounds.getWidthInPixel(parentContainerBounds, windowLayoutInfo)
+            val heightInPixel =
+                sanitizedBounds.getHeightInPixel(parentContainerBounds, windowLayoutInfo)
+            val taskWidth = parentContainerBounds.width
+            val taskHeight = parentContainerBounds.height
+
+            if (widthInPixel == taskWidth && heightInPixel == taskHeight) {
+                return Bounds.EMPTY_BOUNDS
+            }
+
+            // 3. Offset the bounds by position:
+            //     - For top or bottom position, the bounds should attach to the top or bottom of
+            //       the parent task bounds and centered by the middle of the width.
+            //     - For left or right position, the bounds should attach to the left or right of
+            //       the parent task bounds and centered by the middle of the height.
+            return Bounds(0, 0, widthInPixel, heightInPixel).let { bounds ->
+                when (embeddingBounds.alignment) {
+                    ALIGN_TOP -> bounds.offset(((taskWidth - widthInPixel) / 2), 0)
+                    ALIGN_LEFT -> bounds.offset(0, ((taskHeight - heightInPixel) / 2))
+                    ALIGN_BOTTOM ->
+                        bounds.offset(((taskWidth - widthInPixel) / 2), taskHeight - heightInPixel)
+                    ALIGN_RIGHT ->
+                        bounds.offset(taskWidth - widthInPixel, ((taskHeight - heightInPixel) / 2))
+                    else ->
+                        throw IllegalArgumentException(
+                            "Unknown alignment: ${embeddingBounds.alignment}"
+                        )
+                }
+            }
+        }
+
+        private fun Bounds.offset(dx: Int, dy: Int): Bounds =
+            Bounds(left + dx, top + dy, right + dx, bottom + dy)
+
+        /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
+        internal fun translateEmbeddingBounds(
+            embeddingBounds: EmbeddingBounds,
+            parentContainerInfo: ParentContainerInfo,
+        ): Bounds =
+            translateEmbeddingBounds(
+                embeddingBounds,
+                parentContainerInfo.windowBounds,
+                parentContainerInfo.windowLayoutInfo
+            )
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
index fd35178..e982f91 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -17,23 +17,27 @@
 package androidx.window.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
 import android.content.Context
-import android.os.IBinder
+import android.os.Bundle
 import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
 import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowSdkExtensions
 import androidx.window.core.BuildConfig
 import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
 import androidx.window.core.VerificationMode
 import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
 import androidx.window.extensions.WindowExtensionsProvider
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
 import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
 import androidx.window.reflection.Consumer2
 import java.lang.reflect.Proxy
+import java.util.concurrent.Executor
 
 /**
  * Adapter implementation for different historical versions of activity embedding OEM interface in
@@ -43,9 +47,15 @@
     private val embeddingExtension: ActivityEmbeddingComponent,
     private val adapter: EmbeddingAdapter,
     private val consumerAdapter: ConsumerAdapter,
-    private val applicationContext: Context
+    private val applicationContext: Context,
+    @get:VisibleForTesting internal val overlayController: OverlayControllerImpl?,
+    private val activityWindowInfoCallbackController: ActivityWindowInfoCallbackController?,
 ) : EmbeddingInterfaceCompat {
 
+    private val windowSdkExtensions = WindowSdkExtensions.getInstance()
+
+    private var isCustomSplitAttributeCalculatorSet: Boolean = false
+
     override fun setRules(rules: Set<EmbeddingRule>) {
         var hasSplitRule = false
         for (rule in rules) {
@@ -74,70 +84,238 @@
     }
 
     override fun setEmbeddingCallback(embeddingCallback: EmbeddingCallbackInterface) {
-        if (ExtensionsUtil.safeVendorApiLevel < 2) {
-            consumerAdapter.addConsumer(embeddingExtension, List::class, "setSplitInfoCallback") {
-                values ->
-                val splitInfoList = values.filterIsInstance<OEMSplitInfo>()
-                embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
-            }
-        } else {
-            val callback =
-                Consumer2<List<OEMSplitInfo>> { splitInfoList ->
+        when (windowSdkExtensions.extensionVersion) {
+            1 -> {
+                consumerAdapter.addConsumer(
+                    embeddingExtension,
+                    List::class,
+                    "setSplitInfoCallback"
+                ) { values ->
+                    val splitInfoList = values.filterIsInstance<OEMSplitInfo>()
                     embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
                 }
-            embeddingExtension.setSplitInfoCallback(callback)
+            }
+            in 2..4 -> {
+                registerSplitInfoCallback(embeddingCallback)
+            }
+            in 5..Int.MAX_VALUE -> {
+                registerSplitInfoCallback(embeddingCallback)
+
+                // Register ActivityStack callback
+                val activityStackCallback =
+                    Consumer2<List<OEMActivityStack>> { activityStacks ->
+                        embeddingCallback.onActivityStackChanged(adapter.translate(activityStacks))
+                    }
+                embeddingExtension.registerActivityStackCallback(
+                    Runnable::run,
+                    activityStackCallback
+                )
+            }
         }
     }
 
+    private fun registerSplitInfoCallback(embeddingCallback: EmbeddingCallbackInterface) {
+        val splitInfoCallback =
+            Consumer2<List<OEMSplitInfo>> { splitInfoList ->
+                embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
+            }
+        embeddingExtension.setSplitInfoCallback(splitInfoCallback)
+    }
+
     override fun isActivityEmbedded(activity: Activity): Boolean {
         return embeddingExtension.isActivityEmbedded(activity)
     }
 
+    @RequiresWindowSdkExtension(5)
+    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+        windowSdkExtensions.requireExtensionVersion(5)
+        return embeddingExtension.pinTopActivityStack(
+            taskId,
+            adapter.translateSplitPinRule(applicationContext, splitPinRule)
+        )
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun unpinTopActivityStack(taskId: Int) {
+        windowSdkExtensions.requireExtensionVersion(5)
+        return embeddingExtension.unpinTopActivityStack(taskId)
+    }
+
     @RequiresWindowSdkExtension(2)
     override fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
     ) {
-        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+        windowSdkExtensions.requireExtensionVersion(2)
 
         embeddingExtension.setSplitAttributesCalculator(
             adapter.translateSplitAttributesCalculator(calculator)
         )
+        isCustomSplitAttributeCalculatorSet = true
     }
 
     @RequiresWindowSdkExtension(2)
     override fun clearSplitAttributesCalculator() {
-        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
+        windowSdkExtensions.requireExtensionVersion(2)
 
         embeddingExtension.clearSplitAttributesCalculator()
+        isCustomSplitAttributeCalculatorSet = false
+        setDefaultSplitAttributeCalculatorIfNeeded()
     }
 
-    @RequiresWindowSdkExtension(3)
-    override fun invalidateTopVisibleSplitAttributes() {
-        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+    @RequiresWindowSdkExtension(5)
+    override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+        windowSdkExtensions.requireExtensionVersion(5)
+
+        embeddingExtension.finishActivityStacksWithTokens(
+            activityStacks.mapTo(mutableSetOf()) { it.getToken() }
+        )
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+        windowSdkExtensions.requireExtensionVersion(5)
+        adapter.embeddingConfiguration = embeddingConfig
+        setDefaultSplitAttributeCalculatorIfNeeded()
 
         embeddingExtension.invalidateTopVisibleSplitAttributes()
     }
 
-    @Suppress("DEPRECATION")
-    @RequiresWindowSdkExtension(3)
-    override fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
-        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
-
-        embeddingExtension.updateSplitAttributes(
-            splitInfo.token,
-            adapter.translateSplitAttributes(splitAttributes)
-        )
+    private fun setDefaultSplitAttributeCalculatorIfNeeded() {
+        // Setting a default SplitAttributeCalculator if the EmbeddingConfiguration is set,
+        // in order to ensure the dimAreaBehavior in the SplitAttribute is up-to-date.
+        if (
+            windowSdkExtensions.extensionVersion >= 5 &&
+                !isCustomSplitAttributeCalculatorSet &&
+                adapter.embeddingConfiguration != null
+        ) {
+            embeddingExtension.setSplitAttributesCalculator { params ->
+                adapter.translateSplitAttributes(adapter.translate(params.defaultSplitAttributes))
+            }
+        }
     }
 
-    @Suppress("DEPRECATION")
     @RequiresWindowSdkExtension(3)
-    override fun setLaunchingActivityStack(
-        options: ActivityOptions,
-        token: IBinder
-    ): ActivityOptions {
-        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
+    override fun invalidateVisibleActivityStacks() {
+        windowSdkExtensions.requireExtensionVersion(3)
 
-        return embeddingExtension.setLaunchingActivityStack(options, token)
+        embeddingExtension.invalidateVisibleActivityStacks()
+    }
+
+    /**
+     * Updates top [activityStacks][ActivityStack] layouts, which will trigger [SplitAttributes]
+     * calculator and [ActivityStackAttributes] calculator if set.
+     */
+    private fun ActivityEmbeddingComponent.invalidateVisibleActivityStacks() {
+        // Note that this API also updates overlay container regardless of its naming.
+        invalidateTopVisibleSplitAttributes()
+    }
+
+    @Suppress("Deprecation") // To compat with device with extension version 3 and 4.
+    @RequiresWindowSdkExtension(3)
+    override fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
+        windowSdkExtensions.requireExtensionVersion(3)
+
+        if (windowSdkExtensions.extensionVersion >= 5) {
+            embeddingExtension.updateSplitAttributes(
+                splitInfo.getToken(),
+                adapter.translateSplitAttributes(splitAttributes)
+            )
+        } else {
+            embeddingExtension.updateSplitAttributes(
+                splitInfo.getBinder(),
+                adapter.translateSplitAttributes(splitAttributes)
+            )
+        }
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle {
+        windowSdkExtensions.requireExtensionVersion(5)
+
+        ActivityEmbeddingOptionsImpl.setActivityStackToken(options, activityStack.getToken())
+        return options
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun setOverlayCreateParams(
+        options: Bundle,
+        overlayCreateParams: OverlayCreateParams
+    ): Bundle =
+        options.apply {
+            ActivityEmbeddingOptionsImpl.setOverlayCreateParams(options, overlayCreateParams)
+        }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    ) {
+        windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        overlayController!!.overlayAttributesCalculator = calculator
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun clearOverlayAttributesCalculator() {
+        windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        overlayController!!.overlayAttributesCalculator = null
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+        windowSdkExtensions.requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        overlayController!!.updateOverlayAttributes(overlayTag, overlayAttributes)
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: JetpackConsumer<OverlayInfo>,
+    ) {
+        overlayController?.addOverlayInfoCallback(
+            overlayTag,
+            executor,
+            overlayInfoCallback,
+        )
+            ?: apply {
+                Log.w(TAG, "overlayInfo is not supported on device less than version 5")
+
+                overlayInfoCallback.accept(
+                    OverlayInfo(
+                        overlayTag,
+                        currentOverlayAttributes = null,
+                        activityStack = null,
+                    )
+                )
+            }
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer<OverlayInfo>) {
+        overlayController?.removeOverlayInfoCallback(overlayInfoCallback)
+    }
+
+    @RequiresWindowSdkExtension(6)
+    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+        activity: Activity,
+        callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+    ) {
+        activityWindowInfoCallbackController?.addCallback(activity, callback)
+            ?: apply {
+                Log.w(
+                    TAG,
+                    "EmbeddedActivityWindowInfo is not supported on device less than version 6"
+                )
+            }
+    }
+
+    @RequiresWindowSdkExtension(6)
+    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+        callback: JetpackConsumer<EmbeddedActivityWindowInfo>
+    ) {
+        activityWindowInfoCallbackController?.removeCallback(callback)
     }
 
     companion object {
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
new file mode 100644
index 0000000..930acb2
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.RequiresWindowSdkExtension
+
+/**
+ * Configurations of Activity Embedding environment that defines how the embedded Activities behave.
+ *
+ * @constructor The [EmbeddingConfiguration] constructor. The properties are undefined if not
+ *   specified.
+ * @property dimAreaBehavior The requested dim area behavior.
+ * @see ActivityEmbeddingController.setEmbeddingConfiguration
+ */
+class EmbeddingConfiguration
+@JvmOverloads
+constructor(
+    @RequiresWindowSdkExtension(5) val dimAreaBehavior: DimAreaBehavior = DimAreaBehavior.UNDEFINED
+) {
+    /**
+     * The area of dimming to apply.
+     *
+     * @see [android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND]
+     */
+    class DimAreaBehavior private constructor(@IntRange(from = 0, to = 2) internal val value: Int) {
+        companion object {
+            /**
+             * The dim area is not defined.
+             *
+             * This is the default value while building a [EmbeddingConfiguration]. This would also
+             * keep the existing dim area configuration of the current Activity Embedding
+             * environment unchanged when [ActivityEmbeddingController.setEmbeddingConfiguration] is
+             * called.
+             *
+             * @see ActivityEmbeddingController.setEmbeddingConfiguration
+             */
+            @JvmField val UNDEFINED = DimAreaBehavior(0)
+
+            /**
+             * The dim effect is applying on the [ActivityStack] of the Activity window when needed.
+             * If the [ActivityStack] is split and displayed side-by-side with another
+             * [ActivityStack], the dim effect is applying only on the [ActivityStack] of the
+             * requested Activity.
+             */
+            @JvmField val ON_ACTIVITY_STACK = DimAreaBehavior(1)
+
+            /**
+             * The dimming effect is applying on the area of the whole Task when needed. If the
+             * embedded transparent activity is split and displayed side-by-side with another
+             * activity, the dim effect is applying on the Task, which across over the two
+             * [ActivityStack]s.
+             *
+             * This is the default dim area configuration of the Activity Embedding environment,
+             * before the [DimAreaBehavior] is explicitly set by
+             * [ActivityEmbeddingController.setEmbeddingConfiguration].
+             */
+            @JvmField val ON_TASK = DimAreaBehavior(2)
+        }
+
+        override fun toString(): String {
+            return "DimAreaBehavior=" +
+                when (value) {
+                    0 -> "UNDEFINED"
+                    1 -> "ON_ACTIVITY_STACK"
+                    2 -> "ON_TASK"
+                    else -> "UNKNOWN"
+                }
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is EmbeddingConfiguration) return false
+
+        if (dimAreaBehavior != other.dimAreaBehavior) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return dimAreaBehavior.hashCode()
+    }
+
+    override fun toString(): String = "EmbeddingConfiguration{$dimAreaBehavior}"
+
+    /** Builder for creating an instance of [EmbeddingConfiguration]. */
+    class Builder {
+        private var mDimAreaBehavior = DimAreaBehavior.UNDEFINED
+
+        /**
+         * Sets the dim area behavior. By default, the [DimAreaBehavior.UNDEFINED] is used if not
+         * set.
+         *
+         * @param area The dim area.
+         * @return This [Builder]
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        fun setDimAreaBehavior(area: DimAreaBehavior): Builder = apply { mDimAreaBehavior = area }
+
+        /**
+         * Builds a[EmbeddingConfiguration] instance.
+         *
+         * @return The new [EmbeddingConfiguration] instance.
+         */
+        fun build(): EmbeddingConfiguration = EmbeddingConfiguration(mDimAreaBehavior)
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
index 8c63f2a..39ac592 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -17,10 +17,12 @@
 package androidx.window.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
-import android.os.IBinder
+import android.os.Bundle
+import androidx.core.util.Consumer
 import androidx.window.RequiresWindowSdkExtension
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import java.util.concurrent.Executor
 
 /**
  * Adapter interface for different historical versions of activity embedding OEM interface in
@@ -34,10 +36,17 @@
 
     interface EmbeddingCallbackInterface {
         fun onSplitInfoChanged(splitInfo: List<SplitInfo>)
+
+        fun onActivityStackChanged(activityStacks: List<ActivityStack>)
     }
 
     fun isActivityEmbedded(activity: Activity): Boolean
 
+    @RequiresWindowSdkExtension(5)
+    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
+
+    @RequiresWindowSdkExtension(5) fun unpinTopActivityStack(taskId: Int)
+
     @RequiresWindowSdkExtension(2)
     fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -45,11 +54,50 @@
 
     @RequiresWindowSdkExtension(2) fun clearSplitAttributesCalculator()
 
-    @RequiresWindowSdkExtension(3)
-    fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
+    @RequiresWindowSdkExtension(5)
+    fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
 
-    @RequiresWindowSdkExtension(3) fun invalidateTopVisibleSplitAttributes()
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
+
+    @RequiresWindowSdkExtension(5) fun finishActivityStacks(activityStacks: Set<ActivityStack>)
+
+    @RequiresWindowSdkExtension(5)
+    fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
+
+    @RequiresWindowSdkExtension(3) fun invalidateVisibleActivityStacks()
 
     @RequiresWindowSdkExtension(3)
     fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    )
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION) fun clearOverlayAttributesCalculator()
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: Consumer<OverlayInfo>,
+    )
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>)
+
+    @RequiresWindowSdkExtension(6)
+    fun addEmbeddedActivityWindowInfoCallbackForActivity(
+        activity: Activity,
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    )
+
+    @RequiresWindowSdkExtension(6)
+    fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    )
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
index 11e0f41..aaac205 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -17,11 +17,10 @@
 package androidx.window.embedding
 
 import android.app.Activity
-import android.app.ActivityOptions
 import android.content.Context
 import android.content.pm.PackageManager
 import android.os.Build
-import android.os.IBinder
+import android.os.Bundle
 import android.util.Log
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
@@ -30,13 +29,14 @@
 import androidx.core.util.Consumer
 import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowProperties
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.BuildConfig
 import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
 import androidx.window.core.PredicateAdapter
 import androidx.window.core.VerificationMode
 import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
 import androidx.window.embedding.ExtensionEmbeddingBackend.Api31Impl.isSplitPropertyEnabled
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
@@ -52,11 +52,11 @@
 ) : EmbeddingBackend {
 
     @VisibleForTesting val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
-    private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
+    private val embeddingCallback = EmbeddingCallbackImpl()
 
     init {
         splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
-        embeddingExtension?.setEmbeddingCallback(splitInfoEmbeddingCallback)
+        embeddingExtension?.setEmbeddingCallback(embeddingCallback)
     }
 
     companion object {
@@ -87,18 +87,30 @@
             applicationContext: Context
         ): EmbeddingInterfaceCompat? {
             var impl: EmbeddingInterfaceCompat? = null
+            val version = WindowSdkExtensions.getInstance().extensionVersion
             try {
                 if (
-                    isExtensionVersionSupported(ExtensionsUtil.safeVendorApiLevel) &&
-                        EmbeddingCompat.isEmbeddingAvailable()
+                    isExtensionVersionSupported(version) && EmbeddingCompat.isEmbeddingAvailable()
                 ) {
                     impl =
                         EmbeddingBackend::class.java.classLoader?.let { loader ->
+                            val embeddingExtension = EmbeddingCompat.embeddingComponent()
+                            val adapter = EmbeddingAdapter(PredicateAdapter(loader))
                             EmbeddingCompat(
-                                EmbeddingCompat.embeddingComponent(),
-                                EmbeddingAdapter(PredicateAdapter(loader)),
+                                embeddingExtension,
+                                adapter,
                                 ConsumerAdapter(loader),
-                                applicationContext
+                                applicationContext,
+                                if (version >= OVERLAY_FEATURE_VERSION) {
+                                    OverlayControllerImpl(embeddingExtension, adapter)
+                                } else {
+                                    null
+                                },
+                                if (version >= 6) {
+                                    ActivityWindowInfoCallbackController(embeddingExtension)
+                                } else {
+                                    null
+                                },
                             )
                         }
                     // TODO(b/190433400): Check API conformance
@@ -278,11 +290,7 @@
 
             val callbackWrapper = SplitListenerWrapper(activity, executor, callback)
             splitChangeCallbacks.add(callbackWrapper)
-            if (splitInfoEmbeddingCallback.lastInfo != null) {
-                callbackWrapper.accept(splitInfoEmbeddingCallback.lastInfo!!)
-            } else {
-                callbackWrapper.accept(emptyList())
-            }
+            callbackWrapper.accept(embeddingCallback.lastInfo)
         }
     }
 
@@ -298,11 +306,13 @@
     }
 
     /**
-     * Extension callback implementation of the split information. Keeps track of last reported
+     * Extension callback implementation of the embedding information. Keeps track of last reported
      * values.
      */
     internal inner class EmbeddingCallbackImpl : EmbeddingCallbackInterface {
-        var lastInfo: List<SplitInfo>? = null
+        var lastInfo: List<SplitInfo> = emptyList()
+
+        var lastActivityStacks: List<ActivityStack> = emptyList()
 
         override fun onSplitInfoChanged(splitInfo: List<SplitInfo>) {
             lastInfo = splitInfo
@@ -310,6 +320,10 @@
                 callbackWrapper.accept(splitInfo)
             }
         }
+
+        override fun onActivityStackChanged(activityStacks: List<ActivityStack>) {
+            lastActivityStacks = activityStacks
+        }
     }
 
     private fun areExtensionsAvailable(): Boolean {
@@ -336,6 +350,16 @@
         return embeddingExtension?.isActivityEmbedded(activity) ?: false
     }
 
+    @RequiresWindowSdkExtension(5)
+    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+        return embeddingExtension?.pinTopActivityStack(taskId, splitPinRule) ?: false
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun unpinTopActivityStack(taskId: Int) {
+        embeddingExtension?.unpinTopActivityStack(taskId)
+    }
+
     @RequiresWindowSdkExtension(2)
     override fun setSplitAttributesCalculator(
         calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
@@ -348,33 +372,51 @@
         globalLock.withLock { embeddingExtension?.clearSplitAttributesCalculator() }
     }
 
-    override fun getActivityStack(activity: Activity): ActivityStack? {
+    override fun getActivityStack(activity: Activity): ActivityStack? =
         globalLock.withLock {
-            val lastInfo: List<SplitInfo> = splitInfoEmbeddingCallback.lastInfo ?: return null
-            for (info in lastInfo) {
-                if (activity !in info) {
-                    continue
-                }
-                if (activity in info.primaryActivityStack) {
-                    return info.primaryActivityStack
-                }
-                if (activity in info.secondaryActivityStack) {
-                    return info.secondaryActivityStack
-                }
-            }
-            return null
+            embeddingCallback.lastActivityStacks.find { activityStack -> activity in activityStack }
+                ?: getActivityStackFromSplitInfoList(activity)
         }
+
+    @GuardedBy("globalLock")
+    private fun getActivityStackFromSplitInfoList(activity: Activity): ActivityStack? {
+        for (info in embeddingCallback.lastInfo) {
+            if (activity !in info) {
+                continue
+            }
+            if (activity in info.primaryActivityStack) {
+                return info.primaryActivityStack
+            }
+            if (activity in info.secondaryActivityStack) {
+                return info.secondaryActivityStack
+            }
+        }
+        return null
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle =
+        embeddingExtension?.setLaunchingActivityStack(options, activityStack) ?: options
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun setOverlayCreateParams(
+        options: Bundle,
+        overlayCreateParams: OverlayCreateParams,
+    ): Bundle = embeddingExtension?.setOverlayCreateParams(options, overlayCreateParams) ?: options
+
+    @RequiresWindowSdkExtension(5)
+    override fun finishActivityStacks(activityStacks: Set<ActivityStack>) {
+        embeddingExtension?.finishActivityStacks(activityStacks)
+    }
+
+    @RequiresWindowSdkExtension(5)
+    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
+        embeddingExtension?.setEmbeddingConfiguration(embeddingConfig)
     }
 
     @RequiresWindowSdkExtension(3)
-    override fun setLaunchingActivityStack(
-        options: ActivityOptions,
-        token: IBinder
-    ): ActivityOptions = embeddingExtension?.setLaunchingActivityStack(options, token) ?: options
-
-    @RequiresWindowSdkExtension(3)
-    override fun invalidateTopVisibleSplitAttributes() {
-        embeddingExtension?.invalidateTopVisibleSplitAttributes()
+    override fun invalidateVisibleActivityStacks() {
+        embeddingExtension?.invalidateVisibleActivityStacks()
     }
 
     @RequiresWindowSdkExtension(3)
@@ -382,6 +424,60 @@
         embeddingExtension?.updateSplitAttributes(splitInfo, splitAttributes)
     }
 
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    ) {
+        embeddingExtension?.setOverlayAttributesCalculator(calculator)
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun clearOverlayAttributesCalculator() {
+        embeddingExtension?.clearOverlayAttributesCalculator()
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+        embeddingExtension?.updateOverlayAttributes(overlayTag, overlayAttributes)
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: Consumer<OverlayInfo>,
+    ) {
+        embeddingExtension?.addOverlayInfoCallback(overlayTag, executor, overlayInfoCallback)
+            // Send an empty OverlayInfo if the extension does not exist.
+            ?: overlayInfoCallback.accept(
+                OverlayInfo(
+                    overlayTag,
+                    currentOverlayAttributes = null,
+                    activityStack = null,
+                )
+            )
+    }
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer<OverlayInfo>) {
+        embeddingExtension?.removeOverlayInfoCallback(overlayInfoCallback)
+    }
+
+    @RequiresWindowSdkExtension(6)
+    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
+        activity: Activity,
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    ) {
+        embeddingExtension?.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
+    }
+
+    @RequiresWindowSdkExtension(6)
+    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
+        callback: Consumer<EmbeddedActivityWindowInfo>
+    ) {
+        embeddingExtension?.removeEmbeddedActivityWindowInfoCallbackForActivity(callback)
+    }
+
     @RequiresApi(31)
     private object Api31Impl {
         fun isSplitPropertyEnabled(context: Context): SplitController.SplitSupportStatus {
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
new file mode 100644
index 0000000..6c446102
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.annotation.RestrictTo
+
+/**
+ * The attributes to describe how an overlay container should look like.
+ *
+ * @constructor creates an overlay attributes.
+ * @property bounds The overlay container's [EmbeddingBounds], which defaults to
+ *   [EmbeddingBounds.BOUNDS_EXPANDED] if not specified.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayAttributes
+@JvmOverloads
+constructor(val bounds: EmbeddingBounds = EmbeddingBounds.BOUNDS_EXPANDED) {
+
+    override fun toString(): String =
+        "${OverlayAttributes::class.java.simpleName}: {" + "bounds=$bounds" + "}"
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is OverlayAttributes) return false
+        return bounds == other.bounds
+    }
+
+    override fun hashCode(): Int = bounds.hashCode()
+
+    /** The [OverlayAttributes] builder. */
+    class Builder {
+
+        private var bounds = EmbeddingBounds.BOUNDS_EXPANDED
+
+        /**
+         * Sets the overlay bounds, which defaults to [EmbeddingBounds.BOUNDS_EXPANDED] if not
+         * specified.
+         *
+         * @param bounds The [EmbeddingBounds] of the overlay [ActivityStack].
+         * @return The [OverlayAttributes] builder.
+         */
+        fun setBounds(bounds: EmbeddingBounds): Builder = apply { this.bounds = bounds }
+
+        /** Builds [OverlayAttributes]. */
+        fun build(): OverlayAttributes = OverlayAttributes(bounds)
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
new file mode 100644
index 0000000..d357bf9
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.content.res.Configuration
+import androidx.annotation.RestrictTo
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * The parameter container used to report the current device and window state in
+ * [OverlayController.setOverlayAttributesCalculator] and references the corresponding overlay
+ * [ActivityStack] by [overlayTag].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayAttributesCalculatorParams
+internal constructor(
+    /** The parent container's [WindowMetrics] */
+    val parentWindowMetrics: WindowMetrics,
+    /** The parent container's [Configuration] */
+    val parentConfiguration: Configuration,
+    /** The parent container's [WindowLayoutInfo] */
+    val parentWindowLayoutInfo: WindowLayoutInfo,
+    /**
+     * The unique identifier of the overlay [ActivityStack] specified by [OverlayCreateParams.tag]
+     */
+    val overlayTag: String,
+    /**
+     * The overlay [ActivityStack]'s [OverlayAttributes] specified by [overlayTag], which is the
+     * [OverlayAttributes] that is not calculated by calculator. It should be either initialized by
+     * [OverlayCreateParams.overlayAttributes] or [OverlayController.updateOverlayAttributes].
+     */
+    val defaultOverlayAttributes: OverlayAttributes,
+) {
+    override fun toString(): String =
+        "${OverlayAttributesCalculatorParams::class.java}:{" +
+            "parentWindowMetrics=$parentWindowMetrics" +
+            "parentConfiguration=$parentConfiguration" +
+            "parentWindowLayoutInfo=$parentWindowLayoutInfo" +
+            "overlayTag=$overlayTag" +
+            "defaultOverlayAttributes=$defaultOverlayAttributes"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayController.kt b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
new file mode 100644
index 0000000..e2b347c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+/**
+ * The controller to manage overlay [ActivityStack], which is launched by the activityOptions that
+ * [setOverlayCreateParams].
+ *
+ * See linked sample below for how to launch an [android.app.Activity] into an overlay
+ * [ActivityStack].
+ *
+ * Supported operations are:
+ * - [setOverlayAttributesCalculator] to update overlay presentation with device or window state and
+ *   [OverlayCreateParams.tag].
+ *
+ * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayController
+@VisibleForTesting
+internal constructor(private val backend: EmbeddingBackend) {
+
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    internal fun setOverlayCreateParams(
+        options: Bundle,
+        overlayCreateParams: OverlayCreateParams,
+    ): Bundle = backend.setOverlayCreateParams(options, overlayCreateParams)
+
+    /**
+     * Sets an overlay calculator function to update overlay presentation with device or window
+     * state and [OverlayCreateParams.tag].
+     *
+     * Overlay calculator function is triggered with following scenarios:
+     * - An overlay [ActivityStack] is launched.
+     * - The parent task configuration changes. i.e. orientation change, enter/exit multi-window
+     *   mode or resize apps in multi-window mode.
+     * - Device folding state changes.
+     * - Device is attached to an external display and the app is forwarded to that display.
+     *
+     * If there's no [calculator] set, the overlay presentation will be calculated with the previous
+     * set [OverlayAttributes], either from [OverlayCreateParams] to initialize the overlay
+     * container, or from the runtime API to update the overlay container's [OverlayAttributes].
+     *
+     * See the sample linked below for how to use [OverlayAttributes] calculator
+     *
+     * @param calculator The overlay calculator function to compute [OverlayAttributes] by
+     *   [OverlayAttributesCalculatorParams]. It will replace the previously set if it exists.
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 6.
+     * @sample androidx.window.samples.embedding.overlayAttributesCalculatorSample
+     */
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun setOverlayAttributesCalculator(
+        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
+    ) {
+        backend.setOverlayAttributesCalculator(calculator)
+    }
+
+    /**
+     * Clears the overlay calculator function previously set by [setOverlayAttributesCalculator].
+     *
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 6.
+     */
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun clearOverlayAttributesCalculator() {
+        backend.clearOverlayAttributesCalculator()
+    }
+
+    /**
+     * Updates [OverlayAttributes] of the overlay [ActivityStack] specified by [overlayTag]. It's no
+     * op if there's no such overlay [ActivityStack] associated with [overlayTag].
+     *
+     * If an [OverlayAttributes] calculator function is specified, the updated [overlayAttributes]
+     * will be passed by [OverlayAttributesCalculatorParams.defaultOverlayAttributes] when the
+     * calculator function applies to the overlay [ActivityStack] specified by [overlayTag].
+     *
+     * In most cases it is suggested to use
+     * [ActivityEmbeddingController.invalidateVisibleActivityStacks] if a calculator has been set
+     * through [OverlayController.setOverlayAttributesCalculator].
+     *
+     * @param overlayTag The overlay [ActivityStack]'s tag
+     * @param overlayAttributes The [OverlayAttributes] to update
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 6.
+     */
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
+        backend.updateOverlayAttributes(overlayTag, overlayAttributes)
+    }
+
+    /**
+     * A [Flow] of [OverlayInfo] that [overlayTag] is associated with.
+     *
+     * If there's an active overlay [ActivityStack] associated with [overlayTag], it will be
+     * reported in [OverlayInfo.activityStack]. Otherwise, [OverlayInfo.activityStack] is `null`.
+     *
+     * Note that launching an overlay [ActivityStack] only supports on the device with
+     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6. If
+     * [WindowSdkExtensions.extensionVersion] is less than 6, this flow will always report
+     * [OverlayInfo] without associated [OverlayInfo.activityStack].
+     *
+     * @param overlayTag The overlay [ActivityStack]'s tag which is set through
+     *   [OverlayCreateParams]
+     * @return a [Flow] of [OverlayInfo] this [overlayTag] is associated with
+     */
+    @RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+    fun overlayInfo(overlayTag: String): Flow<OverlayInfo> = callbackFlow {
+        val listener = Consumer { info: OverlayInfo -> trySend(info) }
+        backend.addOverlayInfoCallback(overlayTag, Runnable::run, listener)
+        awaitClose { backend.removeOverlayInfoCallback(listener) }
+    }
+
+    companion object {
+
+        internal const val OVERLAY_FEATURE_VERSION = 8
+
+        /**
+         * Obtains an instance of [OverlayController].
+         *
+         * @param context the [Context] to initialize the controller with
+         */
+        @JvmStatic
+        fun getInstance(context: Context): OverlayController {
+            val backend = EmbeddingBackend.getInstance(context)
+            return OverlayController(backend)
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
new file mode 100644
index 0000000..885c283d
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.content.res.Configuration
+import android.util.ArrayMap
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.embedding.ActivityEmbeddingOptionsImpl.getOverlayAttributes
+import androidx.window.embedding.ActivityEmbeddingOptionsImpl.putActivityStackAlignment
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
+import androidx.window.extensions.embedding.ParentContainerInfo
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
+import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
+import androidx.window.layout.util.DensityCompatHelper
+import androidx.window.reflection.Consumer2
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * The core implementation of [OverlayController] APIs, which is implemented by [ActivityStack]
+ * operations in WM Extensions.
+ */
+@Suppress("NewApi") // Suppress #translateWindowMetrics, which requires R.
+@RequiresWindowSdkExtension(OVERLAY_FEATURE_VERSION)
+internal open class OverlayControllerImpl(
+    private val embeddingExtension: ActivityEmbeddingComponent,
+    private val adapter: EmbeddingAdapter,
+) {
+    private val globalLock = ReentrantLock()
+
+    @GuardedBy("globalLock")
+    internal var overlayAttributesCalculator:
+        ((OverlayAttributesCalculatorParams) -> OverlayAttributes)? =
+        null
+        get() = globalLock.withLock { field }
+        set(value) {
+            globalLock.withLock { field = value }
+        }
+
+    /**
+     * Mapping between the overlay container tag and its default [OverlayAttributes]. It's to record
+     * the [OverlayAttributes] updated through [updateOverlayAttributes] and report in
+     * [OverlayAttributesCalculatorParams].
+     */
+    @GuardedBy("globalLock")
+    private val overlayTagToDefaultAttributesMap: MutableMap<String, OverlayAttributes> = ArrayMap()
+
+    /**
+     * Mapping between the overlay container tag and its current [OverlayAttributes] to provide the
+     * [OverlayInfo] updates.
+     */
+    @GuardedBy("globalLock")
+    private val overlayTagToCurrentAttributesMap = ArrayMap<String, OverlayAttributes>()
+
+    @GuardedBy("globalLock")
+    private val overlayTagToContainerMap = ArrayMap<String, ActivityStack>()
+
+    /** The mapping from [OverlayInfo] callback to [activityStacks][ActivityStack] callback. */
+    @GuardedBy("globalLock")
+    private val overlayInfoToActivityStackCallbackMap =
+        ArrayMap<JetpackConsumer<OverlayInfo>, Consumer<List<ActivityStack>>>()
+
+    init {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        embeddingExtension.setActivityStackAttributesCalculator { params ->
+            globalLock.withLock {
+                val parentContainerInfo = params.parentContainerInfo
+                val density =
+                    DensityCompatHelper.getInstance()
+                        .density(
+                            parentContainerInfo.configuration,
+                            parentContainerInfo.windowMetrics
+                        )
+                val windowMetrics =
+                    WindowMetricsCalculator.translateWindowMetrics(
+                        parentContainerInfo.windowMetrics,
+                        density
+                    )
+                val overlayAttributes =
+                    calculateOverlayAttributes(
+                        params.activityStackTag,
+                        params.launchOptions.getOverlayAttributes(),
+                        WindowMetricsCalculator.translateWindowMetrics(
+                            params.parentContainerInfo.windowMetrics,
+                            density
+                        ),
+                        params.parentContainerInfo.configuration,
+                        ExtensionsWindowLayoutInfoAdapter.translate(
+                            windowMetrics,
+                            parentContainerInfo.windowLayoutInfo
+                        ),
+                    )
+
+                // TODO(b/295805497): Migrate to either custom animation APIs or new
+                //  ActivityStackAttributes APIs.
+                // Set alignment to the bundle options as the hint of the animation direction.
+                params.launchOptions.putActivityStackAlignment(overlayAttributes.bounds)
+                return@setActivityStackAttributesCalculator overlayAttributes
+                    .toActivityStackAttributes(parentContainerInfo)
+            }
+        }
+
+        embeddingExtension.registerActivityStackCallback(Runnable::run) { activityStacks ->
+            globalLock.withLock {
+                val lastOverlayTags = overlayTagToContainerMap.keys
+
+                overlayTagToContainerMap.clear()
+                overlayTagToContainerMap.putAll(
+                    activityStacks.getOverlayContainers().map { overlayContainer ->
+                        Pair(overlayContainer.tag!!, overlayContainer)
+                    }
+                )
+
+                cleanUpDismissedOverlayContainerRecords(lastOverlayTags)
+            }
+        }
+    }
+
+    /**
+     * Clean up records associated with dismissed overlay [activityStacks][ActivityStack] when
+     * there's a [ActivityStack] state update.
+     *
+     * The dismissed overlay [activityStacks][ActivityStack] are identified by comparing the
+     * differences of [ActivityStack] state before and after update.
+     *
+     * @param lastOverlayTags Overlay containers' tag before applying [ActivityStack] state update.
+     */
+    @GuardedBy("globalLock")
+    private fun cleanUpDismissedOverlayContainerRecords(lastOverlayTags: Set<String>) {
+        if (lastOverlayTags.isEmpty()) {
+            // If there's no last overlay container, return.
+            return
+        }
+
+        val dismissedOverlayTags = ArrayList<String>()
+        val currentOverlayTags = overlayTagToContainerMap.keys
+
+        for (overlayTag in lastOverlayTags) {
+            if (
+                overlayTag !in currentOverlayTags &&
+                    // If an overlay activityStack is not in the current overlay container list,
+                    // check
+                    // whether the activityStack does really not exist in WM Extensions in case
+                    // an overlay container is just launched, but th WM Jetpack hasn't received the
+                    // update yet.
+                    embeddingExtension.getActivityStackToken(overlayTag) == null
+            ) {
+                dismissedOverlayTags.add(overlayTag)
+            }
+        }
+
+        for (overlayTag in dismissedOverlayTags) {
+            overlayTagToDefaultAttributesMap.remove(overlayTag)
+            overlayTagToCurrentAttributesMap.remove(overlayTag)
+        }
+    }
+
+    /**
+     * Calculates the [OverlayAttributes] to report to the [ActivityStackAttributes] calculator.
+     *
+     * The calculator then computes [ActivityStackAttributes] for rendering the overlay
+     * [ActivityStack].
+     *
+     * @param tag The overlay [ActivityStack].
+     * @param initialOverlayAttrs The [OverlayCreateParams.overlayAttributes] that used to launching
+     *   this overlay [ActivityStack]
+     * @param windowMetrics The parent window container's [WindowMetrics]
+     * @param configuration The parent window container's [Configuration]
+     * @param windowLayoutInfo The parent window container's [WindowLayoutInfo]
+     */
+    @VisibleForTesting
+    internal fun calculateOverlayAttributes(
+        tag: String,
+        initialOverlayAttrs: OverlayAttributes?,
+        windowMetrics: WindowMetrics,
+        configuration: Configuration,
+        windowLayoutInfo: WindowLayoutInfo,
+    ): OverlayAttributes {
+        val defaultOverlayAttrs =
+            getUpdatedOverlayAttributes(tag)
+                ?: initialOverlayAttrs
+                ?: throw IllegalArgumentException(
+                    "Can't retrieve overlay attributes from launch options"
+                )
+        val currentOverlayAttrs =
+            overlayAttributesCalculator?.invoke(
+                OverlayAttributesCalculatorParams(
+                    windowMetrics,
+                    configuration,
+                    windowLayoutInfo,
+                    tag,
+                    defaultOverlayAttrs,
+                )
+            ) ?: defaultOverlayAttrs
+
+        overlayTagToCurrentAttributesMap[tag] = currentOverlayAttrs
+
+        return currentOverlayAttrs
+    }
+
+    @VisibleForTesting
+    internal open fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =
+        overlayTagToDefaultAttributesMap[overlayTag]
+
+    internal open fun updateOverlayAttributes(
+        overlayTag: String,
+        overlayAttributes: OverlayAttributes
+    ) {
+        globalLock.withLock {
+            val activityStackToken =
+                overlayTagToContainerMap[overlayTag]?.activityStackToken
+                    // Users may call this API before any callback coming. Try to ask platform if
+                    // this container exists.
+                    ?: embeddingExtension.getActivityStackToken(overlayTag)
+                    // Early return if there's no such ActivityStack associated with the tag.
+                    ?: return
+
+            embeddingExtension.updateActivityStackAttributes(
+                activityStackToken,
+                overlayAttributes.toActivityStackAttributes(
+                    embeddingExtension.getParentContainerInfo(activityStackToken)!!
+                )
+            )
+
+            // Update the tag-overlayAttributes map, which will be treated as the default
+            // overlayAttributes in calculator.
+            overlayTagToDefaultAttributesMap[overlayTag] = overlayAttributes
+            overlayTagToCurrentAttributesMap[overlayTag] = overlayAttributes
+        }
+    }
+
+    private fun OverlayAttributes.toActivityStackAttributes(
+        parentContainerInfo: ParentContainerInfo
+    ): ActivityStackAttributes =
+        ActivityStackAttributes.Builder()
+            .setRelativeBounds(
+                EmbeddingBounds.translateEmbeddingBounds(
+                        bounds,
+                        adapter.translate(parentContainerInfo)
+                    )
+                    .toRect()
+            )
+            .setWindowAttributes(adapter.translateWindowAttributes())
+            .build()
+
+    private fun List<ActivityStack>.getOverlayContainers(): List<ActivityStack> =
+        filter { activityStack -> activityStack.tag != null }.toList()
+
+    open fun addOverlayInfoCallback(
+        overlayTag: String,
+        executor: Executor,
+        overlayInfoCallback: JetpackConsumer<OverlayInfo>,
+    ) {
+        globalLock.withLock {
+            val callback =
+                Consumer2<List<ActivityStack>> { activityStacks ->
+                    val overlayInfoList =
+                        activityStacks.filter { activityStack -> activityStack.tag == overlayTag }
+                    if (overlayInfoList.size > 1) {
+                        throw IllegalStateException(
+                            "There must be at most one overlay ActivityStack with $overlayTag"
+                        )
+                    }
+                    val overlayInfo =
+                        if (overlayInfoList.isEmpty()) {
+                            OverlayInfo(
+                                overlayTag,
+                                currentOverlayAttributes = null,
+                                activityStack = null,
+                            )
+                        } else {
+                            overlayInfoList.first().toOverlayInfo()
+                        }
+                    overlayInfoCallback.accept(overlayInfo)
+                }
+            overlayInfoToActivityStackCallbackMap[overlayInfoCallback] = callback
+
+            embeddingExtension.registerActivityStackCallback(executor, callback)
+        }
+    }
+
+    private fun ActivityStack.toOverlayInfo(): OverlayInfo =
+        OverlayInfo(
+            tag!!,
+            overlayTagToCurrentAttributesMap[tag!!],
+            adapter.translate(this),
+        )
+
+    open fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer<OverlayInfo>) {
+        globalLock.withLock {
+            val callback = overlayInfoToActivityStackCallbackMap.remove(overlayInfoCallback)
+            if (callback != null) {
+                embeddingExtension.unregisterActivityStackCallback(callback)
+            }
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
new file mode 100644
index 0000000..25548c0
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.annotation.RestrictTo
+import java.util.UUID
+
+/**
+ * The parameter container to create an overlay [ActivityStack].
+ *
+ * If there's an shown overlay [ActivityStack] associated with the [tag], the existing
+ * [ActivityStack] will be:
+ * - Dismissed if the overlay [ActivityStack] is in different task from the launched one
+ * - Updated with [OverlayAttributes] if the overlay [ActivityStack] is in the same task.
+ *
+ * See [android.os.Bundle.setOverlayCreateParams] for how to create an overlay [ActivityStack].
+ *
+ * @constructor creates a parameter container to launch an overlay [ActivityStack].
+ * @property tag The unique identifier of the overlay [ActivityStack], which will be generated
+ *   automatically if not specified.
+ * @property overlayAttributes The attributes of the overlay [ActivityStack], which defaults to the
+ *   default value of [OverlayAttributes].
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayCreateParams
+@JvmOverloads
+constructor(
+    val tag: String = generateOverlayTag(),
+    val overlayAttributes: OverlayAttributes = OverlayAttributes.Builder().build(),
+) {
+    override fun toString(): String =
+        "${OverlayCreateParams::class.simpleName}:{ " +
+            ", tag=$tag" +
+            ", attrs=$overlayAttributes" +
+            "}"
+
+    /** The [OverlayCreateParams] builder. */
+    class Builder {
+        private var tag: String? = null
+        private var launchAttrs: OverlayAttributes? = null
+
+        /**
+         * Sets the overlay [ActivityStack]'s unique identifier. The builder will generate one
+         * automatically if not specified.
+         *
+         * @param tag The unique identifier of the overlay [ActivityStack] to create.
+         * @return The [OverlayCreateParams] builder.
+         */
+        fun setTag(tag: String): Builder = apply { this.tag = tag }
+
+        /**
+         * Sets the overlay [ActivityStack]'s attributes, which defaults to the default value of
+         * [OverlayAttributes.Builder].
+         *
+         * @param attrs The [OverlayAttributes].
+         * @return The [OverlayCreateParams] builder.
+         */
+        fun setOverlayAttributes(attrs: OverlayAttributes): Builder = apply { launchAttrs = attrs }
+
+        /** Builds the [OverlayCreateParams] */
+        fun build(): OverlayCreateParams =
+            OverlayCreateParams(
+                tag ?: generateOverlayTag(),
+                launchAttrs ?: OverlayAttributes.Builder().build()
+            )
+    }
+
+    companion object {
+
+        /** A helper function to generate a random unique identifier. */
+        @JvmStatic
+        fun generateOverlayTag(): String = UUID.randomUUID().toString().substring(IntRange(0, 32))
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
new file mode 100644
index 0000000..6f76742
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.app.Activity
+import androidx.annotation.RestrictTo
+
+/** Describes an overlay [ActivityStack] associated with [OverlayCreateParams.tag]. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class OverlayInfo
+internal constructor(
+    /** The unique identifier associated with the overlay [ActivityStack]. */
+    val overlayTag: String,
+    /**
+     * The [OverlayAttributes] of the overlay [ActivityStack] if it exists, or `null` if there's no
+     * such a [ActivityStack]
+     */
+    val currentOverlayAttributes: OverlayAttributes?,
+    /**
+     * The overlay [ActivityStack] associated with [overlayTag], or `null` if there's no such a
+     * [ActivityStack].
+     */
+    val activityStack: ActivityStack?
+) {
+    operator fun contains(activity: Activity): Boolean = activityStack?.contains(activity) ?: false
+
+    override fun toString(): String =
+        "${OverlayInfo::class.java.simpleName}: {" +
+            "tag=$overlayTag" +
+            ", currentOverlayAttrs=$currentOverlayAttributes" +
+            ", activityStack=$activityStack" +
+            "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
new file mode 100644
index 0000000..9edb443
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.content.res.Configuration
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.core.Bounds
+import androidx.window.layout.WindowLayoutInfo
+
+/**
+ * The parent container information directly passed from WM Extensions, which is created to make
+ * test implementation easier.
+ *
+ * @property windowBounds The parent container's [Bounds].
+ * @property windowLayoutInfo The parent container's [WindowLayoutInfo].
+ * @property windowInsets The parent container's [WindowInsetsCompat].
+ * @property configuration The parent container's [Configuration].
+ * @property density The parent container's density in DP, which has the same unit as
+ *   [android.util.DisplayMetrics.density].
+ */
+internal data class ParentContainerInfo(
+    /** The parent container's [Bounds]. */
+    val windowBounds: Bounds,
+    /** The parent container's [WindowLayoutInfo]. */
+    val windowLayoutInfo: WindowLayoutInfo,
+    /** The parent container's [WindowInsetsCompat] */
+    val windowInsets: WindowInsetsCompat,
+    /** The parent container's [Configuration]. */
+    val configuration: Configuration,
+    /**
+     * The parent container's density in DP, which has the same unit as
+     * [android.util.DisplayMetrics.density].
+     */
+    val density: Float,
+)
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
index 55905cc..4484493 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -23,6 +23,7 @@
 import android.content.res.XmlResourceParser
 import androidx.annotation.XmlRes
 import androidx.window.R
+import androidx.window.embedding.EmbeddingAnimationParams.AnimationSpec.Companion.DEFAULT
 import androidx.window.embedding.EmbeddingAspectRatio.Companion.buildAspectRatioFromValue
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
 import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
@@ -168,6 +169,37 @@
                     ALWAYS.value
                 )
             val clearTop = typedArray.getBoolean(R.styleable.SplitPairRule_clearTop, false)
+            val animationBackgroundColor =
+                typedArray.getColor(R.styleable.SplitPairRule_animationBackgroundColor, 0)
+            val openAnimation =
+                typedArray.getInt(R.styleable.SplitPairRule_splitOpenAnimation, DEFAULT.value)
+            val closeAnimation =
+                typedArray.getInt(R.styleable.SplitPairRule_splitCloseAnimation, DEFAULT.value)
+            val changeAnimation =
+                typedArray.getInt(R.styleable.SplitPairRule_splitChangeAnimation, DEFAULT.value)
+            typedArray.recycle()
+
+            val animationParams =
+                EmbeddingAnimationParams.Builder()
+                    .setAnimationBackground(
+                        EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
+                    )
+                    .setOpenAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            openAnimation
+                        )
+                    )
+                    .setCloseAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            closeAnimation
+                        )
+                    )
+                    .setChangeAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            changeAnimation
+                        )
+                    )
+                    .build()
 
             val defaultAttrs =
                 SplitAttributes.Builder()
@@ -175,6 +207,7 @@
                     .setLayoutDirection(
                         SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
                     )
+                    .setAnimationParams(animationParams)
                     .build()
 
             SplitPairRule.Builder(emptySet())
@@ -249,6 +282,46 @@
                     R.styleable.SplitPlaceholderRule_splitLayoutDirection,
                     LOCALE.value
                 )
+            val animationBackgroundColor =
+                typedArray.getColor(R.styleable.SplitPlaceholderRule_animationBackgroundColor, 0)
+            val openAnimation =
+                typedArray.getInt(
+                    R.styleable.SplitPlaceholderRule_splitOpenAnimation,
+                    DEFAULT.value
+                )
+            val closeAnimation =
+                typedArray.getInt(
+                    R.styleable.SplitPlaceholderRule_splitCloseAnimation,
+                    DEFAULT.value
+                )
+            val changeAnimation =
+                typedArray.getInt(
+                    R.styleable.SplitPlaceholderRule_splitChangeAnimation,
+                    DEFAULT.value
+                )
+            typedArray.recycle()
+
+            val animationParams =
+                EmbeddingAnimationParams.Builder()
+                    .setAnimationBackground(
+                        EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
+                    )
+                    .setOpenAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            openAnimation
+                        )
+                    )
+                    .setCloseAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            closeAnimation
+                        )
+                    )
+                    .setChangeAnimation(
+                        EmbeddingAnimationParams.AnimationSpec.getAnimationSpecFromValue(
+                            changeAnimation
+                        )
+                    )
+                    .build()
 
             val defaultAttrs =
                 SplitAttributes.Builder()
@@ -256,6 +329,7 @@
                     .setLayoutDirection(
                         SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
                     )
+                    .setAnimationParams(animationParams)
                     .build()
             val packageName = context.applicationContext.packageName
             val placeholderActivityClassName =
diff --git a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
index 400ff6a..8ff5de4 100644
--- a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
@@ -18,25 +18,44 @@
 
 import android.app.Activity
 import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.IBinder
+import android.view.WindowMetrics
 import androidx.annotation.VisibleForTesting
 import androidx.window.SafeWindowExtensionsProvider
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.WindowExtensions
 import androidx.window.extensions.core.util.function.Consumer
 import androidx.window.extensions.core.util.function.Function
+import androidx.window.extensions.core.util.function.Predicate
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
 import androidx.window.extensions.embedding.ActivityRule
 import androidx.window.extensions.embedding.ActivityStack
+import androidx.window.extensions.embedding.ActivityStackAttributes
+import androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams
+import androidx.window.extensions.embedding.AnimationBackground
+import androidx.window.extensions.embedding.AnimationParams
+import androidx.window.extensions.embedding.DividerAttributes
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo
+import androidx.window.extensions.embedding.ParentContainerInfo
 import androidx.window.extensions.embedding.SplitAttributes
 import androidx.window.extensions.embedding.SplitAttributes.SplitType
+import androidx.window.extensions.embedding.SplitAttributesCalculatorParams
 import androidx.window.extensions.embedding.SplitInfo
 import androidx.window.extensions.embedding.SplitPairRule
+import androidx.window.extensions.embedding.SplitPinRule
 import androidx.window.extensions.embedding.SplitPlaceholderRule
+import androidx.window.extensions.embedding.SplitRule
+import androidx.window.extensions.embedding.WindowAttributes
+import androidx.window.extensions.layout.WindowLayoutInfo
 import androidx.window.reflection.ReflectionUtils.doesReturn
 import androidx.window.reflection.ReflectionUtils.isPublic
 import androidx.window.reflection.ReflectionUtils.validateReflection
 import androidx.window.reflection.WindowExtensionsConstants.ACTIVITY_EMBEDDING_COMPONENT_CLASS
+import java.util.concurrent.Executor
 
 /**
  * Reflection Guard for [ActivityEmbeddingComponent]. This will go through the
@@ -69,10 +88,13 @@
         }
         // TODO(b/267573854) : update logic to fallback to lower version
         //  if higher version is not matched
-        return when (ExtensionsUtil.safeVendorApiLevel) {
+        return when (WindowSdkExtensions.getInstance().extensionVersion) {
             1 -> hasValidVendorApiLevel1()
-            in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
-            // TODO(b/267956499) : add  hasValidVendorApiLevel3
+            2 -> hasValidVendorApiLevel2()
+            in 3..4 -> hasValidVendorApiLevel3() // No additional API in 4.
+            5 -> hasValidVendorApiLevel5()
+            6 -> hasValidVendorApiLevel6()
+            in 7..Int.MAX_VALUE -> hasValidVendorApiLevel7()
             else -> false
         }
     }
@@ -83,25 +105,34 @@
             isActivityEmbeddingComponentValid()
 
     /**
-     * [WindowExtensions.VENDOR_API_LEVEL_1] includes the following methods:
+     * Vendor API level 1 includes the following methods:
      * - [ActivityEmbeddingComponent.setEmbeddingRules]
      * - [ActivityEmbeddingComponent.isActivityEmbedded]
-     * - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer] and
-     *   following classes:
+     * - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer]
+     * - [SplitRule.getSplitRatio]
+     * - [SplitRule.getLayoutDirection] and following classes:
      * - [ActivityRule]
+     * - [ActivityRule.Builder]
      * - [SplitInfo]
      * - [SplitPairRule]
+     * - [SplitPairRule.Builder]
      * - [SplitPlaceholderRule]
+     * - [SplitPlaceholderRule.Builder]
      */
     @VisibleForTesting
     internal fun hasValidVendorApiLevel1(): Boolean {
         return isMethodSetEmbeddingRulesValid() &&
             isMethodIsActivityEmbeddedValid() &&
             isMethodSetSplitInfoCallbackJavaConsumerValid() &&
+            isMethodGetSplitRatioValid() &&
+            isMethodGetLayoutDirectionValid() &&
             isClassActivityRuleValid() &&
+            isClassActivityRuleBuilderLevel1Valid() &&
             isClassSplitInfoValid() &&
             isClassSplitPairRuleValid() &&
-            isClassSplitPlaceholderRuleValid()
+            isClassSplitPairRuleBuilderLevel1Valid() &&
+            isClassSplitPlaceholderRuleValid() &&
+            isClassSplitPlaceholderRuleBuilderLevel1Valid()
     }
 
     /**
@@ -110,9 +141,16 @@
      * - [ActivityEmbeddingComponent.clearSplitInfoCallback]
      * - [ActivityEmbeddingComponent.setSplitAttributesCalculator]
      * - [ActivityEmbeddingComponent.clearSplitAttributesCalculator]
-     * - [SplitInfo.getSplitAttributes] and following classes:
+     * - [SplitInfo.getSplitAttributes]
+     * - [SplitPlaceholderRule.getFinishPrimaryWithPlaceholder]
+     * - [SplitRule.getDefaultSplitAttributes] and following classes:
+     * - [ActivityRule.Builder]
+     * - [EmbeddingRule]
      * - [SplitAttributes]
      * - [SplitAttributes.SplitType]
+     * - [SplitAttributesCalculatorParams]
+     * - [SplitPairRule.Builder]
+     * - [SplitPlaceholderRule.Builder]
      */
     @VisibleForTesting
     internal fun hasValidVendorApiLevel2(): Boolean {
@@ -121,10 +159,154 @@
             isMethodClearSplitInfoCallbackValid() &&
             isMethodSplitAttributesCalculatorValid() &&
             isMethodGetSplitAttributesValid() &&
+            isMethodGetFinishPrimaryWithPlaceholderValid() &&
+            isMethodGetDefaultSplitAttributesValid() &&
+            isClassActivityRuleBuilderLevel2Valid() &&
+            isClassEmbeddingRuleValid() &&
             isClassSplitAttributesValid() &&
-            isClassSplitTypeValid()
+            isClassSplitAttributesCalculatorParamsValid() &&
+            isClassSplitTypeValid() &&
+            isClassSplitPairRuleBuilderLevel2Valid() &&
+            isClassSplitPlaceholderRuleBuilderLevel2Valid()
     }
 
+    /**
+     * Vendor API level 3 includes the following methods:
+     * - [ActivityEmbeddingComponent.updateSplitAttributes]
+     * - [ActivityEmbeddingComponent.invalidateTopVisibleSplitAttributes]
+     * - [SplitInfo.getToken]
+     */
+    @VisibleForTesting
+    internal fun hasValidVendorApiLevel3(): Boolean =
+        hasValidVendorApiLevel2() &&
+            isMethodInvalidateTopVisibleSplitAttributesValid() &&
+            isMethodUpdateSplitAttributesValid() &&
+            isMethodSplitInfoGetTokenValid()
+
+    /**
+     * Vendor API level 5 includes the following methods:
+     * - [ActivityEmbeddingComponent.registerActivityStackCallback]
+     * - [ActivityEmbeddingComponent.unregisterActivityStackCallback]
+     * - [ActivityStack.getActivityStackToken]
+     * - [ActivityStack.Token.createFromBinder]
+     * - [ActivityStack.Token.readFromBundle]
+     * - [ActivityStack.Token.toBundle]
+     * - [ActivityStack.Token.INVALID_ACTIVITY_STACK_TOKEN]
+     * - [AnimationBackground.createColorBackground]
+     * - [AnimationBackground.ANIMATION_BACKGROUND_DEFAULT]
+     * - [SplitAttributes.getAnimationBackground]
+     * - [SplitAttributes.Builder.setAnimationBackground]
+     * - [WindowAttributes.getDimAreaBehavior]
+     * - [SplitAttributes.getWindowAttributes]
+     * - [SplitAttributes.Builder.setWindowAttributes]
+     * - [SplitPinRule.isSticky]
+     * - [ActivityEmbeddingComponent.pinTopActivityStack]
+     * - [ActivityEmbeddingComponent.unpinTopActivityStack]
+     * - [ActivityEmbeddingComponent.updateSplitAttributes] with [SplitInfo.Token]
+     * - [SplitInfo.getSplitInfoToken] and following classes:
+     * - [AnimationBackground]
+     * - [ActivityStack.Token]
+     * - [WindowAttributes]
+     * - [SplitInfo.Token]
+     */
+    @VisibleForTesting
+    internal fun hasValidVendorApiLevel5(): Boolean =
+        hasValidVendorApiLevel3() &&
+            isActivityStackGetActivityStackTokenValid() &&
+            isMethodRegisterActivityStackCallbackValid() &&
+            isMethodUnregisterActivityStackCallbackValid() &&
+            isMethodPinUnpinTopActivityStackValid() &&
+            isMethodUpdateSplitAttributesWithTokenValid() &&
+            isMethodGetSplitInfoTokenValid() &&
+            isClassAnimationBackgroundValid() &&
+            isClassActivityStackTokenValid() &&
+            isClassWindowAttributesValid() &&
+            isClassSplitInfoTokenValid()
+
+    /**
+     * Vendor API level 6 includes the following methods:
+     * - [ActivityEmbeddingComponent.clearEmbeddedActivityWindowInfoCallback]
+     * - [ActivityEmbeddingComponent.getEmbeddedActivityWindowInfo]
+     * - [ActivityEmbeddingComponent.setEmbeddedActivityWindowInfoCallback]
+     * - [SplitAttributes.getDividerAttributes]
+     * - [SplitAttributes.Builder.setDividerAttributes] and following classes:
+     * - [EmbeddedActivityWindowInfo]
+     * - [DividerAttributes]
+     * - [DividerAttributes.Builder]
+     */
+    @VisibleForTesting
+    internal fun hasValidVendorApiLevel6(): Boolean =
+        hasValidVendorApiLevel5() &&
+            isMethodGetEmbeddedActivityWindowInfoValid() &&
+            isMethodSetEmbeddedActivityWindowInfoCallbackValid() &&
+            isMethodClearEmbeddedActivityWindowInfoCallbackValid() &&
+            isMethodGetDividerAttributesValid() &&
+            isMethodSetDividerAttributesValid() &&
+            isClassEmbeddedActivityWindowInfoValid() &&
+            isClassDividerAttributesValid() &&
+            isClassDividerAttributesBuilderValid()
+
+    /**
+     * Vendor API level 7 includes the following methods:
+     * - [SplitAttributes.getAnimationParams]
+     * - [SplitAttributes.Builder.setAnimationParams]
+     * - [DividerAttributes.isDraggingToFullscreenAllowed]
+     * - [DividerAttributes.Builder.setDraggingToFullscreenAllowed] and following classes:
+     * - [AnimationParams]
+     * - [AnimationParams.Builder]
+     */
+    @VisibleForTesting
+    internal fun hasValidVendorApiLevel7(): Boolean =
+        hasValidVendorApiLevel6() &&
+            isMethodGetAnimationParamsValid() &&
+            isMethodSetAnimationParamsValid() &&
+            isMethodIsDraggingToFullscreenAllowedValid() &&
+            isMethodSetDraggingToFullscreenAllowedValid() &&
+            isClassAnimationParamsValid() &&
+            isClassAnimationParamsBuilderValid()
+
+    /**
+     * Overlay features includes the following methods:
+     * - [ActivityEmbeddingComponent.clearActivityStackAttributesCalculator]
+     * - [ActivityEmbeddingComponent.getActivityStackToken]
+     * - [ActivityEmbeddingComponent.getParentContainerInfo]
+     * - [ActivityEmbeddingComponent.setActivityStackAttributesCalculator]
+     * - [ActivityEmbeddingComponent.updateActivityStackAttributes]
+     * - [ActivityStack.getTag] and following classes:
+     * - [ParentContainerInfo]
+     * - [ActivityStackAttributes]
+     * - [ActivityStackAttributes.Builder]
+     * - [ActivityStackAttributesCalculatorParams]
+     */
+    private fun isOverlayFeatureValid(): Boolean =
+        isActivityStackGetTagValid() &&
+            isMethodGetActivityStackTokenValid() &&
+            isClassParentContainerInfoValid() &&
+            isMethodGetParentContainerInfoValid() &&
+            isMethodSetActivityStackAttributesCalculatorValid() &&
+            isMethodClearActivityStackAttributesCalculatorValid() &&
+            isMethodUpdateActivityStackAttributesValid() &&
+            isClassActivityStackAttributesValid() &&
+            isClassActivityStackAttributesBuilderValid() &&
+            isClassActivityStackAttributesCalculatorParamsValid()
+
+    private val activityEmbeddingComponentClass: Class<*>
+        get() {
+            return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
+        }
+
+    private fun isActivityEmbeddingComponentValid(): Boolean {
+        return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
+            val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
+            val getActivityEmbeddingComponentMethod =
+                extensionsClass.getMethod("getActivityEmbeddingComponent")
+            val activityEmbeddingComponentClass = activityEmbeddingComponentClass
+            getActivityEmbeddingComponentMethod.isPublic &&
+                getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
+        }
+    }
+
+    /** Vendor API level 1 validation methods */
     private fun isMethodSetEmbeddingRulesValid(): Boolean {
         return validateReflection("ActivityEmbeddingComponent#setEmbeddingRules is not valid") {
             val setEmbeddingRulesMethod =
@@ -145,6 +327,146 @@
         }
     }
 
+    private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
+        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
+            val consumerClass =
+                consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
+            val setSplitInfoCallbackMethod =
+                activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
+            setSplitInfoCallbackMethod.isPublic
+        }
+    }
+
+    private fun isMethodGetSplitRatioValid(): Boolean =
+        validateReflection("SplitRule#getSplitRatio is not valid") {
+            val splitRuleClass = SplitRule::class.java
+            val getSplitRatioMethod = splitRuleClass.getMethod("getSplitRatio")
+            getSplitRatioMethod.isPublic && getSplitRatioMethod.doesReturn(Float::class.java)
+        }
+
+    private fun isMethodGetLayoutDirectionValid(): Boolean =
+        validateReflection("SplitRule#getLayoutDirection is not valid") {
+            val splitRuleClass = SplitRule::class.java
+            val getLayoutDirectionMethod = splitRuleClass.getMethod("getLayoutDirection")
+            getLayoutDirectionMethod.isPublic &&
+                getLayoutDirectionMethod.doesReturn(Int::class.java)
+        }
+
+    private fun isClassActivityRuleValid(): Boolean =
+        validateReflection("Class ActivityRule is not valid") {
+            val activityRuleClass = ActivityRule::class.java
+            val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
+            shouldAlwaysExpandMethod.isPublic &&
+                shouldAlwaysExpandMethod.doesReturn(Boolean::class.java)
+        }
+
+    private fun isClassActivityRuleBuilderLevel1Valid(): Boolean =
+        validateReflection("Class ActivityRule.Builder is not valid") {
+            val activityRuleBuilderClass = ActivityRule.Builder::class.java
+            val setShouldAlwaysExpandMethod =
+                activityRuleBuilderClass.getMethod("setShouldAlwaysExpand", Boolean::class.java)
+            setShouldAlwaysExpandMethod.isPublic &&
+                setShouldAlwaysExpandMethod.doesReturn(ActivityRule.Builder::class.java)
+        }
+
+    private fun isClassSplitInfoValid(): Boolean =
+        validateReflection("Class SplitInfo is not valid") {
+            val splitInfoClass = SplitInfo::class.java
+            val getPrimaryActivityStackMethod = splitInfoClass.getMethod("getPrimaryActivityStack")
+            val getSecondaryActivityStackMethod =
+                splitInfoClass.getMethod("getSecondaryActivityStack")
+            val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
+            getPrimaryActivityStackMethod.isPublic &&
+                getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
+                getSecondaryActivityStackMethod.isPublic &&
+                getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
+                getSplitRatioMethod.isPublic &&
+                getSplitRatioMethod.doesReturn(Float::class.java)
+        }
+
+    private fun isClassSplitPairRuleValid(): Boolean =
+        validateReflection("Class SplitPairRule is not valid") {
+            val splitPairRuleClass = SplitPairRule::class.java
+            val getFinishPrimaryWithSecondaryMethod =
+                splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
+            val getFinishSecondaryWithPrimaryMethod =
+                splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
+            val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
+            getFinishPrimaryWithSecondaryMethod.isPublic &&
+                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
+                getFinishSecondaryWithPrimaryMethod.isPublic &&
+                getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
+                shouldClearTopMethod.isPublic &&
+                shouldClearTopMethod.doesReturn(Boolean::class.java)
+        }
+
+    private fun isClassSplitPairRuleBuilderLevel1Valid(): Boolean =
+        validateReflection("Class SplitPairRule.Builder is not valid") {
+            val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
+            val setSplitRatioMethod =
+                splitPairRuleBuilderClass.getMethod("setSplitRatio", Float::class.java)
+            val setLayoutDirectionMethod =
+                splitPairRuleBuilderClass.getMethod("setLayoutDirection", Int::class.java)
+            setSplitRatioMethod.isPublic &&
+                setSplitRatioMethod.doesReturn(SplitPairRule.Builder::class.java) &&
+                setLayoutDirectionMethod.isPublic &&
+                setLayoutDirectionMethod.doesReturn(SplitPairRule.Builder::class.java)
+        }
+
+    private fun isClassSplitPlaceholderRuleValid(): Boolean =
+        validateReflection("Class SplitPlaceholderRule is not valid") {
+            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
+            val getPlaceholderIntentMethod =
+                splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
+            val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
+            val getFinishPrimaryWithSecondaryMethod =
+                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
+            getPlaceholderIntentMethod.isPublic &&
+                getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
+                isStickyMethod.isPublic &&
+                isStickyMethod.doesReturn(Boolean::class.java) &&
+                getFinishPrimaryWithSecondaryMethod.isPublic &&
+                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
+        }
+
+    private fun isClassSplitPlaceholderRuleBuilderLevel1Valid(): Boolean =
+        validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
+            val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
+            val setSplitRatioMethod =
+                splitPlaceholderRuleBuilderClass.getMethod("setSplitRatio", Float::class.java)
+            val setLayoutDirectionMethod =
+                splitPlaceholderRuleBuilderClass.getMethod("setLayoutDirection", Int::class.java)
+            val setStickyMethod =
+                splitPlaceholderRuleBuilderClass.getMethod("setSticky", Boolean::class.java)
+            val setFinishPrimaryWithSecondaryMethod =
+                splitPlaceholderRuleBuilderClass.getMethod(
+                    "setFinishPrimaryWithSecondary",
+                    Int::class.java
+                )
+            setSplitRatioMethod.isPublic &&
+                setSplitRatioMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+                setLayoutDirectionMethod.isPublic &&
+                setLayoutDirectionMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+                setStickyMethod.isPublic &&
+                setStickyMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
+                setFinishPrimaryWithSecondaryMethod.isPublic &&
+                setFinishPrimaryWithSecondaryMethod.doesReturn(
+                    SplitPlaceholderRule.Builder::class.java
+                )
+        }
+
+    /** Vendor API level 2 validation methods */
+    private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
+        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
+            val setSplitInfoCallbackMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "setSplitInfoCallback",
+                    Consumer::class.java
+                )
+            setSplitInfoCallbackMethod.isPublic
+        }
+    }
+
     private fun isMethodClearSplitInfoCallbackValid(): Boolean {
         return validateReflection(
             "ActivityEmbeddingComponent#clearSplitInfoCallback is not valid"
@@ -179,6 +501,45 @@
                 getSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
         }
 
+    private fun isMethodGetFinishPrimaryWithPlaceholderValid(): Boolean =
+        validateReflection("SplitPlaceholderRule#getFinishPrimaryWithPlaceholder is not valid") {
+            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
+            val getFinishPrimaryWithPlaceholderMethod =
+                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithPlaceholder")
+            getFinishPrimaryWithPlaceholderMethod.isPublic &&
+                getFinishPrimaryWithPlaceholderMethod.doesReturn(Int::class.java)
+        }
+
+    private fun isMethodGetDefaultSplitAttributesValid(): Boolean =
+        validateReflection("SplitRule#getDefaultSplitAttributes is not valid") {
+            val splitRuleClass = SplitRule::class.java
+            val getDefaultSplitAttributesMethod =
+                splitRuleClass.getMethod("getDefaultSplitAttributes")
+            getDefaultSplitAttributesMethod.isPublic &&
+                getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
+        }
+
+    private fun isClassActivityRuleBuilderLevel2Valid(): Boolean =
+        validateReflection("Class ActivityRule.Builder is not valid") {
+            val activityRuleBuilderClass = ActivityRule.Builder::class.java
+            val activityRuleBuilderConstructor =
+                activityRuleBuilderClass.getDeclaredConstructor(
+                    Predicate::class.java,
+                    Predicate::class.java
+                )
+            val setTagMethod = activityRuleBuilderClass.getMethod("setTag", String::class.java)
+            activityRuleBuilderConstructor.isPublic &&
+                setTagMethod.isPublic &&
+                setTagMethod.doesReturn(ActivityRule.Builder::class.java)
+        }
+
+    private fun isClassEmbeddingRuleValid(): Boolean =
+        validateReflection("Class EmbeddingRule is not valid") {
+            val embeddingRuleClass = EmbeddingRule::class.java
+            val getTagMethod = embeddingRuleClass.getMethod("getTag")
+            getTagMethod.isPublic && getTagMethod.doesReturn(String::class.java)
+        }
+
     private fun isClassSplitAttributesValid(): Boolean =
         validateReflection("Class SplitAttributes is not valid") {
             val splitAttributesClass = SplitAttributes::class.java
@@ -197,6 +558,36 @@
                 setLayoutDirectionMethod.isPublic
         }
 
+    @Suppress("newApi") // Suppress lint check for WindowMetrics
+    private fun isClassSplitAttributesCalculatorParamsValid(): Boolean =
+        validateReflection("Class SplitAttributesCalculatorParams is not valid") {
+            val splitAttributesCalculatorParamsClass = SplitAttributesCalculatorParams::class.java
+            val getParentWindowMetricsMethod =
+                splitAttributesCalculatorParamsClass.getMethod("getParentWindowMetrics")
+            val getParentConfigurationMethod =
+                splitAttributesCalculatorParamsClass.getMethod("getParentConfiguration")
+            val getDefaultSplitAttributesMethod =
+                splitAttributesCalculatorParamsClass.getMethod("getDefaultSplitAttributes")
+            val areDefaultConstraintsSatisfiedMethod =
+                splitAttributesCalculatorParamsClass.getMethod("areDefaultConstraintsSatisfied")
+            val getParentWindowLayoutInfoMethod =
+                splitAttributesCalculatorParamsClass.getMethod("getParentWindowLayoutInfo")
+            val getSplitRuleTagMethod =
+                splitAttributesCalculatorParamsClass.getMethod("getSplitRuleTag")
+            getParentWindowMetricsMethod.isPublic &&
+                getParentWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
+                getParentConfigurationMethod.isPublic &&
+                getParentConfigurationMethod.doesReturn(Configuration::class.java) &&
+                getDefaultSplitAttributesMethod.isPublic &&
+                getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java) &&
+                areDefaultConstraintsSatisfiedMethod.isPublic &&
+                areDefaultConstraintsSatisfiedMethod.doesReturn(Boolean::class.java) &&
+                getParentWindowLayoutInfoMethod.isPublic &&
+                getParentWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java) &&
+                getSplitRuleTagMethod.isPublic &&
+                getSplitRuleTagMethod.doesReturn(String::class.java)
+        }
+
     private fun isClassSplitTypeValid(): Boolean =
         validateReflection("Class SplitAttributes.SplitType is not valid") {
             val ratioSplitTypeClass = SplitType.RatioSplitType::class.java
@@ -222,99 +613,577 @@
                 expandContainersSplitTypeConstructor.isPublic
         }
 
-    private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
-        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
-            val consumerClass =
-                consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
-            val setSplitInfoCallbackMethod =
-                activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
-            setSplitInfoCallbackMethod.isPublic
-        }
-    }
-
-    private fun isClassActivityRuleValid(): Boolean =
-        validateReflection("Class ActivityRule is not valid") {
-            val activityRuleClass = ActivityRule::class.java
-            val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
-            val activityRuleBuilderClass = ActivityRule.Builder::class.java
-            val setShouldAlwaysExpandMethod =
-                activityRuleBuilderClass.getMethod("setShouldAlwaysExpand", Boolean::class.java)
-            shouldAlwaysExpandMethod.isPublic &&
-                shouldAlwaysExpandMethod.doesReturn(Boolean::class.java) &&
-                setShouldAlwaysExpandMethod.isPublic
+    private fun isClassSplitPairRuleBuilderLevel2Valid(): Boolean =
+        validateReflection("Class SplitPairRule.Builder is not valid") {
+            val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
+            val splitPairRuleBuilderConstructor =
+                splitPairRuleBuilderClass.getDeclaredConstructor(
+                    Predicate::class.java,
+                    Predicate::class.java,
+                    Predicate::class.java
+                )
+            val setDefaultSplitAttributesMethod =
+                splitPairRuleBuilderClass.getMethod(
+                    "setDefaultSplitAttributes",
+                    SplitAttributes::class.java,
+                )
+            val setTagMethod = splitPairRuleBuilderClass.getMethod("setTag", String::class.java)
+            splitPairRuleBuilderConstructor.isPublic &&
+                setDefaultSplitAttributesMethod.isPublic &&
+                setDefaultSplitAttributesMethod.doesReturn(SplitPairRule.Builder::class.java) &&
+                setTagMethod.isPublic &&
+                setTagMethod.doesReturn(SplitPairRule.Builder::class.java)
         }
 
-    private fun isClassSplitInfoValid(): Boolean =
-        validateReflection("Class SplitInfo is not valid") {
-            val splitInfoClass = SplitInfo::class.java
-            val getPrimaryActivityStackMethod = splitInfoClass.getMethod("getPrimaryActivityStack")
-            val getSecondaryActivityStackMethod =
-                splitInfoClass.getMethod("getSecondaryActivityStack")
-            val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
-            getPrimaryActivityStackMethod.isPublic &&
-                getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
-                getSecondaryActivityStackMethod.isPublic &&
-                getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
-                getSplitRatioMethod.isPublic &&
-                getSplitRatioMethod.doesReturn(Float::class.java)
+    private fun isClassSplitPlaceholderRuleBuilderLevel2Valid(): Boolean =
+        validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
+            val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
+            val splitPlaceholderRuleBuilderConstructor =
+                splitPlaceholderRuleBuilderClass.getDeclaredConstructor(
+                    Intent::class.java,
+                    Predicate::class.java,
+                    Predicate::class.java,
+                    Predicate::class.java
+                )
+            val setDefaultSplitAttributesMethod =
+                splitPlaceholderRuleBuilderClass.getMethod(
+                    "setDefaultSplitAttributes",
+                    SplitAttributes::class.java,
+                )
+            val setFinishPrimaryWithPlaceholderMethod =
+                splitPlaceholderRuleBuilderClass.getMethod(
+                    "setFinishPrimaryWithPlaceholder",
+                    Int::class.java
+                )
+            val setTagMethod =
+                splitPlaceholderRuleBuilderClass.getMethod("setTag", String::class.java)
+            splitPlaceholderRuleBuilderConstructor.isPublic &&
+                setDefaultSplitAttributesMethod.isPublic &&
+                setDefaultSplitAttributesMethod.doesReturn(
+                    SplitPlaceholderRule.Builder::class.java
+                ) &&
+                setFinishPrimaryWithPlaceholderMethod.isPublic &&
+                setFinishPrimaryWithPlaceholderMethod.doesReturn(
+                    SplitPlaceholderRule.Builder::class.java
+                ) &&
+                setTagMethod.isPublic &&
+                setTagMethod.doesReturn(SplitPlaceholderRule.Builder::class.java)
         }
 
-    private fun isClassSplitPairRuleValid(): Boolean =
-        validateReflection("Class SplitPairRule is not valid") {
-            val splitPairRuleClass = SplitPairRule::class.java
-            val getFinishPrimaryWithSecondaryMethod =
-                splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
-            val getFinishSecondaryWithPrimaryMethod =
-                splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
-            val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
-            getFinishPrimaryWithSecondaryMethod.isPublic &&
-                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
-                getFinishSecondaryWithPrimaryMethod.isPublic &&
-                getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
-                shouldClearTopMethod.isPublic &&
-                shouldClearTopMethod.doesReturn(Boolean::class.java)
+    /** Vendor API level 3 validation methods */
+    private fun isMethodInvalidateTopVisibleSplitAttributesValid(): Boolean =
+        validateReflection("#invalidateTopVisibleSplitAttributes is not valid") {
+            val invalidateTopVisibleSplitAttributesMethod =
+                activityEmbeddingComponentClass.getMethod("invalidateTopVisibleSplitAttributes")
+            invalidateTopVisibleSplitAttributesMethod.isPublic
         }
 
-    private fun isClassSplitPlaceholderRuleValid(): Boolean =
-        validateReflection("Class SplitPlaceholderRule is not valid") {
-            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
-            val getPlaceholderIntentMethod =
-                splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
-            val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
-            val getFinishPrimaryWithSecondaryMethod =
-                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
-            getPlaceholderIntentMethod.isPublic &&
-                getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
-                isStickyMethod.isPublic &&
-                isStickyMethod.doesReturn(Boolean::class.java)
-            getFinishPrimaryWithSecondaryMethod.isPublic &&
-                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
-        }
-
-    private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
-        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
-            val setSplitInfoCallbackMethod =
+    private fun isMethodUpdateSplitAttributesValid(): Boolean =
+        validateReflection("#updateSplitAttributes is not valid") {
+            val updateSplitAttributesMethod =
                 activityEmbeddingComponentClass.getMethod(
-                    "setSplitInfoCallback",
+                    "updateSplitAttributes",
+                    IBinder::class.java,
+                    SplitAttributes::class.java
+                )
+            updateSplitAttributesMethod.isPublic
+        }
+
+    private fun isMethodSplitInfoGetTokenValid(): Boolean =
+        validateReflection("SplitInfo#getToken is not valid") {
+            val splitInfoClass = SplitInfo::class.java
+            val getTokenMethod = splitInfoClass.getMethod("getToken")
+            getTokenMethod.isPublic && getTokenMethod.doesReturn(IBinder::class.java)
+        }
+
+    /** Vendor API level 5 validation methods */
+    private fun isActivityStackGetActivityStackTokenValid(): Boolean =
+        validateReflection("ActivityStack#getActivityToken is not valid") {
+            val activityStackClass = ActivityStack::class.java
+            val getActivityStackTokenMethod = activityStackClass.getMethod("getActivityStackToken")
+
+            getActivityStackTokenMethod.isPublic &&
+                getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
+        }
+
+    private fun isMethodRegisterActivityStackCallbackValid(): Boolean =
+        validateReflection("registerActivityStackCallback is not valid") {
+            val registerActivityStackCallbackMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "registerActivityStackCallback",
+                    Executor::class.java,
                     Consumer::class.java
                 )
-            setSplitInfoCallbackMethod.isPublic
+            registerActivityStackCallbackMethod.isPublic
         }
-    }
 
-    private fun isActivityEmbeddingComponentValid(): Boolean {
-        return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
-            val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
-            val getActivityEmbeddingComponentMethod =
-                extensionsClass.getMethod("getActivityEmbeddingComponent")
-            val activityEmbeddingComponentClass = activityEmbeddingComponentClass
-            getActivityEmbeddingComponentMethod.isPublic &&
-                getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
+    private fun isMethodUnregisterActivityStackCallbackValid(): Boolean =
+        validateReflection("unregisterActivityStackCallback is not valid") {
+            val unregisterActivityStackCallbackMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "unregisterActivityStackCallback",
+                    Consumer::class.java
+                )
+            unregisterActivityStackCallbackMethod.isPublic
         }
-    }
 
-    private val activityEmbeddingComponentClass: Class<*>
-        get() {
-            return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
+    private fun isMethodPinUnpinTopActivityStackValid(): Boolean =
+        validateReflection("#pin(unPin)TopActivityStack is not valid") {
+            val splitPinRuleClass = SplitPinRule::class.java
+            val isStickyMethod = splitPinRuleClass.getMethod("isSticky")
+            val pinTopActivityStackMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "pinTopActivityStack",
+                    Int::class.java,
+                    SplitPinRule::class.java
+                )
+            val unpinTopActivityStackMethod =
+                activityEmbeddingComponentClass.getMethod("unpinTopActivityStack", Int::class.java)
+            isStickyMethod.isPublic &&
+                isStickyMethod.doesReturn(Boolean::class.java) &&
+                pinTopActivityStackMethod.isPublic &&
+                pinTopActivityStackMethod.doesReturn(Boolean::class.java) &&
+                unpinTopActivityStackMethod.isPublic
+        }
+
+    private fun isMethodUpdateSplitAttributesWithTokenValid(): Boolean =
+        validateReflection("updateSplitAttributes is not valid") {
+            val updateSplitAttributesMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "updateSplitAttributes",
+                    SplitInfo.Token::class.java,
+                    SplitAttributes::class.java,
+                )
+            updateSplitAttributesMethod.isPublic
+        }
+
+    private fun isMethodGetSplitInfoTokenValid(): Boolean =
+        validateReflection("SplitInfo#getSplitInfoToken is not valid") {
+            val splitInfoClass = SplitInfo::class.java
+            val getSplitInfoToken = splitInfoClass.getMethod("getSplitInfoToken")
+            getSplitInfoToken.isPublic && getSplitInfoToken.doesReturn(SplitInfo.Token::class.java)
+        }
+
+    private fun isClassAnimationBackgroundValid(): Boolean =
+        validateReflection("Class AnimationBackground is not valid") {
+            val animationBackgroundClass = AnimationBackground::class.java
+            val colorBackgroundClass = AnimationBackground.ColorBackground::class.java
+            val createColorBackgroundMethod =
+                animationBackgroundClass.getMethod("createColorBackground", Int::class.java)
+            val animationBackgroundDefaultField =
+                animationBackgroundClass.getDeclaredField("ANIMATION_BACKGROUND_DEFAULT")
+            val colorBackgroundGetColor = colorBackgroundClass.getMethod("getColor")
+
+            val splitAttributesClass = SplitAttributes::class.java
+            val getAnimationBackgroundMethod =
+                splitAttributesClass.getMethod("getAnimationBackground")
+
+            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+            val setAnimationBackgroundMethod =
+                splitAttributesBuilderClass.getMethod(
+                    "setAnimationBackground",
+                    AnimationBackground::class.java
+                )
+
+            createColorBackgroundMethod.isPublic &&
+                createColorBackgroundMethod.doesReturn(colorBackgroundClass) &&
+                animationBackgroundDefaultField.isPublic &&
+                colorBackgroundGetColor.isPublic &&
+                colorBackgroundGetColor.doesReturn(Int::class.java) &&
+                getAnimationBackgroundMethod.isPublic &&
+                getAnimationBackgroundMethod.doesReturn(animationBackgroundClass) &&
+                setAnimationBackgroundMethod.isPublic &&
+                setAnimationBackgroundMethod.doesReturn(SplitAttributes.Builder::class.java)
+        }
+
+    private fun isClassActivityStackTokenValid(): Boolean =
+        validateReflection("Class ActivityStack.Token is not valid") {
+            val activityStackTokenClass = ActivityStack.Token::class.java
+            val toBundleMethod = activityStackTokenClass.getMethod("toBundle")
+            val readFromBundle =
+                activityStackTokenClass.getMethod("readFromBundle", Bundle::class.java)
+            val createFromBinder =
+                activityStackTokenClass.getMethod("createFromBinder", IBinder::class.java)
+            val invalidActivityStackTokenField =
+                activityStackTokenClass.getDeclaredField("INVALID_ACTIVITY_STACK_TOKEN")
+
+            toBundleMethod.isPublic &&
+                toBundleMethod.doesReturn(Bundle::class.java) &&
+                readFromBundle.isPublic &&
+                readFromBundle.doesReturn(activityStackTokenClass) &&
+                createFromBinder.isPublic &&
+                createFromBinder.doesReturn(activityStackTokenClass) &&
+                invalidActivityStackTokenField.isPublic
+        }
+
+    private fun isClassWindowAttributesValid(): Boolean =
+        validateReflection("Class WindowAttributes is not valid") {
+            val windowAttributesClass = WindowAttributes::class.java
+            val getDimAreaBehaviorMethod = windowAttributesClass.getMethod("getDimAreaBehavior")
+
+            val splitAttributesClass = SplitAttributes::class.java
+            val getWindowAttributesMethod = splitAttributesClass.getMethod("getWindowAttributes")
+
+            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+            val setWindowAttributesMethod =
+                splitAttributesBuilderClass.getMethod(
+                    "setWindowAttributes",
+                    WindowAttributes::class.java
+                )
+
+            getDimAreaBehaviorMethod.isPublic &&
+                getDimAreaBehaviorMethod.doesReturn(Int::class.java) &&
+                getWindowAttributesMethod.isPublic &&
+                getWindowAttributesMethod.doesReturn(windowAttributesClass) &&
+                setWindowAttributesMethod.isPublic &&
+                setWindowAttributesMethod.doesReturn(SplitAttributes.Builder::class.java)
+        }
+
+    private fun isClassSplitInfoTokenValid(): Boolean =
+        validateReflection("SplitInfo.Token is not valid") {
+            val splitInfoTokenClass = SplitInfo.Token::class.java
+            val createFromBinder =
+                splitInfoTokenClass.getMethod("createFromBinder", IBinder::class.java)
+
+            createFromBinder.isPublic && createFromBinder.doesReturn(splitInfoTokenClass)
+        }
+
+    /** Vendor API level 6 validation methods */
+    private fun isMethodGetEmbeddedActivityWindowInfoValid(): Boolean =
+        validateReflection(
+            "ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo is not valid"
+        ) {
+            val getEmbeddedActivityWindowInfoMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "getEmbeddedActivityWindowInfo",
+                    Activity::class.java
+                )
+            getEmbeddedActivityWindowInfoMethod.isPublic &&
+                getEmbeddedActivityWindowInfoMethod.doesReturn(
+                    EmbeddedActivityWindowInfo::class.java
+                )
+        }
+
+    private fun isMethodSetEmbeddedActivityWindowInfoCallbackValid(): Boolean =
+        validateReflection(
+            "ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback is not valid"
+        ) {
+            val setEmbeddedActivityWindowInfoCallbackMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "setEmbeddedActivityWindowInfoCallback",
+                    Executor::class.java,
+                    Consumer::class.java
+                )
+            setEmbeddedActivityWindowInfoCallbackMethod.isPublic
+        }
+
+    private fun isMethodClearEmbeddedActivityWindowInfoCallbackValid(): Boolean =
+        validateReflection(
+            "ActivityEmbeddingComponent#clearEmbeddedActivityWindowInfoCallback is not valid"
+        ) {
+            val clearEmbeddedActivityWindowInfoCallbackMethod =
+                activityEmbeddingComponentClass.getMethod("clearEmbeddedActivityWindowInfoCallback")
+            clearEmbeddedActivityWindowInfoCallbackMethod.isPublic
+        }
+
+    private fun isMethodGetDividerAttributesValid(): Boolean =
+        validateReflection("SplitAttributes#getDividerAttributes is not valid") {
+            val splitAttributesClass = SplitAttributes::class.java
+            val getDividerAttributesMethod = splitAttributesClass.getMethod("getDividerAttributes")
+            getDividerAttributesMethod.isPublic &&
+                getDividerAttributesMethod.doesReturn(DividerAttributes::class.java)
+        }
+
+    private fun isMethodSetDividerAttributesValid(): Boolean =
+        validateReflection("SplitAttributes#setDividerAttributes is not valid") {
+            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+            val setDividerAttributesMethod =
+                splitAttributesBuilderClass.getMethod(
+                    "setDividerAttributes",
+                    DividerAttributes::class.java
+                )
+            setDividerAttributesMethod.isPublic &&
+                setDividerAttributesMethod.doesReturn(SplitAttributes.Builder::class.java)
+        }
+
+    private fun isClassEmbeddedActivityWindowInfoValid(): Boolean =
+        validateReflection("Class EmbeddedActivityWindowInfo is not valid") {
+            val embeddedActivityWindowInfoClass = EmbeddedActivityWindowInfo::class.java
+            val getActivityMethod = embeddedActivityWindowInfoClass.getMethod("getActivity")
+            val isEmbeddedMethod = embeddedActivityWindowInfoClass.getMethod("isEmbedded")
+            val getTaskBoundsMethod = embeddedActivityWindowInfoClass.getMethod("getTaskBounds")
+            val getActivityStackBoundsMethod =
+                embeddedActivityWindowInfoClass.getMethod("getActivityStackBounds")
+            getActivityMethod.isPublic &&
+                getActivityMethod.doesReturn(Activity::class.java) &&
+                isEmbeddedMethod.isPublic &&
+                isEmbeddedMethod.doesReturn(Boolean::class.java) &&
+                getTaskBoundsMethod.isPublic &&
+                getTaskBoundsMethod.doesReturn(Rect::class.java) &&
+                getActivityStackBoundsMethod.isPublic &&
+                getActivityStackBoundsMethod.doesReturn(Rect::class.java)
+        }
+
+    private fun isClassDividerAttributesValid(): Boolean =
+        validateReflection("Class DividerAttributes is not valid") {
+            val dividerAttributesClass = DividerAttributes::class.java
+            val getDividerTypeMethod = dividerAttributesClass.getMethod("getDividerType")
+            val getWidthDpMethod = dividerAttributesClass.getMethod("getWidthDp")
+            val getPrimaryMinRatioMethod = dividerAttributesClass.getMethod("getPrimaryMinRatio")
+            val getPrimaryMaxRatioMethod = dividerAttributesClass.getMethod("getPrimaryMaxRatio")
+            val getDividerColorMethod = dividerAttributesClass.getMethod("getDividerColor")
+            getDividerTypeMethod.isPublic &&
+                getDividerTypeMethod.doesReturn(Int::class.java) &&
+                getWidthDpMethod.isPublic &&
+                getWidthDpMethod.doesReturn(Int::class.java) &&
+                getPrimaryMinRatioMethod.isPublic &&
+                getPrimaryMinRatioMethod.doesReturn(Float::class.java) &&
+                getPrimaryMaxRatioMethod.isPublic &&
+                getPrimaryMaxRatioMethod.doesReturn(Float::class.java) &&
+                getDividerColorMethod.isPublic &&
+                getDividerColorMethod.doesReturn(Int::class.java)
+        }
+
+    private fun isClassDividerAttributesBuilderValid(): Boolean =
+        validateReflection("Class DividerAttributes.Builder is not valid") {
+            val dividerAttributesBuilderClass = DividerAttributes.Builder::class.java
+            val dividerAttributesTypeBuilderConstructor =
+                dividerAttributesBuilderClass.getDeclaredConstructor(Int::class.java)
+            val dividerAttributesBuilderConstructor =
+                dividerAttributesBuilderClass.getDeclaredConstructor(DividerAttributes::class.java)
+            val setWidthDpMethod =
+                dividerAttributesBuilderClass.getMethod("setWidthDp", Int::class.java)
+            val setPrimaryMinRatioMethod =
+                dividerAttributesBuilderClass.getMethod("setPrimaryMinRatio", Float::class.java)
+            val setPrimaryMaxRatioMethod =
+                dividerAttributesBuilderClass.getMethod("setPrimaryMaxRatio", Float::class.java)
+            val setDividerColorMethod =
+                dividerAttributesBuilderClass.getMethod("setDividerColor", Int::class.java)
+            dividerAttributesTypeBuilderConstructor.isPublic &&
+                dividerAttributesBuilderConstructor.isPublic &&
+                setWidthDpMethod.isPublic &&
+                setWidthDpMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+                setPrimaryMinRatioMethod.isPublic &&
+                setPrimaryMinRatioMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+                setPrimaryMaxRatioMethod.isPublic &&
+                setPrimaryMaxRatioMethod.doesReturn(DividerAttributes.Builder::class.java) &&
+                setDividerColorMethod.isPublic &&
+                setDividerColorMethod.doesReturn(DividerAttributes.Builder::class.java)
+        }
+
+    /** Vendor API level 7 validation methods */
+    private fun isMethodGetAnimationParamsValid(): Boolean =
+        validateReflection("SplitAttributes#getAnimationParams is not valid") {
+            val splitAttributesClass = SplitAttributes::class.java
+            val getAnimationParamsMethod = splitAttributesClass.getMethod("getAnimationParams")
+            getAnimationParamsMethod.isPublic &&
+                getAnimationParamsMethod.doesReturn(AnimationParams::class.java)
+        }
+
+    private fun isMethodSetAnimationParamsValid(): Boolean =
+        validateReflection("SplitAttributes#setAnimationParams is not valid") {
+            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
+            val setAnimationParamsMethod =
+                splitAttributesBuilderClass.getMethod(
+                    "setAnimationParams",
+                    AnimationParams::class.java
+                )
+            setAnimationParamsMethod.isPublic &&
+                setAnimationParamsMethod.doesReturn(SplitAttributes.Builder::class.java)
+        }
+
+    private fun isMethodIsDraggingToFullscreenAllowedValid(): Boolean =
+        validateReflection("DividerAttributes#isDraggingToFullscreenAllowed is not valid") {
+            val dividerAttributesClass = DividerAttributes::class.java
+            val getDividerTypeMethod =
+                dividerAttributesClass.getMethod("isDraggingToFullscreenAllowed")
+            getDividerTypeMethod.isPublic && getDividerTypeMethod.doesReturn(Boolean::class.java)
+        }
+
+    private fun isMethodSetDraggingToFullscreenAllowedValid(): Boolean =
+        validateReflection(
+            "DividerAttributes.Builder#setDraggingToFullscreenAllowed is not valid"
+        ) {
+            val dividerAttributesBuilderClass = DividerAttributes.Builder::class.java
+            val setDividerColorMethod =
+                dividerAttributesBuilderClass.getMethod(
+                    "setDraggingToFullscreenAllowed",
+                    Boolean::class.java
+                )
+            setDividerColorMethod.isPublic &&
+                setDividerColorMethod.doesReturn(DividerAttributes.Builder::class.java)
+        }
+
+    private fun isClassAnimationParamsValid(): Boolean =
+        validateReflection("Class AnimationParams is not valid") {
+            val animationParamsClass = AnimationParams::class.java
+            val animationResourcesIdDefaultField =
+                animationParamsClass.getDeclaredField("DEFAULT_ANIMATION_RESOURCES_ID")
+            val getAnimationBackgroundMethod =
+                animationParamsClass.getMethod("getAnimationBackground")
+            val getOpenAnimationResIdMethod =
+                animationParamsClass.getMethod("getOpenAnimationResId")
+            val getCloseAnimationResIdMethod =
+                animationParamsClass.getMethod("getCloseAnimationResId")
+            val getChangeAnimationResIdMethod =
+                animationParamsClass.getMethod("getChangeAnimationResId")
+            animationResourcesIdDefaultField.isPublic &&
+                getAnimationBackgroundMethod.isPublic &&
+                getAnimationBackgroundMethod.doesReturn(AnimationBackground::class.java) &&
+                getOpenAnimationResIdMethod.isPublic &&
+                getOpenAnimationResIdMethod.doesReturn(Int::class.java) &&
+                getCloseAnimationResIdMethod.isPublic &&
+                getCloseAnimationResIdMethod.doesReturn(Int::class.java) &&
+                getChangeAnimationResIdMethod.isPublic &&
+                getChangeAnimationResIdMethod.doesReturn(Int::class.java)
+        }
+
+    private fun isClassAnimationParamsBuilderValid(): Boolean =
+        validateReflection("Class AnimationParams.Builder is not valid") {
+            val animationParamsBuilderClass = AnimationParams.Builder::class.java
+            val setAnimationBackgroundMethod =
+                animationParamsBuilderClass.getMethod(
+                    "setAnimationBackground",
+                    AnimationBackground::class.java
+                )
+            val setOpenAnimationResIdMethod =
+                animationParamsBuilderClass.getMethod("setOpenAnimationResId", Int::class.java)
+            val setCloseAnimationResIdMethod =
+                animationParamsBuilderClass.getMethod("setCloseAnimationResId", Int::class.java)
+            val setChangeAnimationResIdMethod =
+                animationParamsBuilderClass.getMethod("setChangeAnimationResId", Int::class.java)
+            setAnimationBackgroundMethod.isPublic &&
+                setAnimationBackgroundMethod.doesReturn(AnimationParams.Builder::class.java) &&
+                setOpenAnimationResIdMethod.isPublic &&
+                setOpenAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java) &&
+                setCloseAnimationResIdMethod.isPublic &&
+                setCloseAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java) &&
+                setChangeAnimationResIdMethod.isPublic &&
+                setChangeAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java)
+        }
+
+    /** Overlay features validation methods */
+    private fun isActivityStackGetTagValid(): Boolean =
+        validateReflection("ActivityStack#getTag is not valid") {
+            val activityStackClass = ActivityStack::class.java
+            val getTokenMethod = activityStackClass.getMethod("getTag")
+
+            getTokenMethod.isPublic && getTokenMethod.doesReturn(String::class.java)
+        }
+
+    private fun isMethodGetActivityStackTokenValid(): Boolean =
+        validateReflection("getActivityStackToken is not valid") {
+            val getActivityStackTokenMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "getActivityStackToken",
+                    String::class.java
+                )
+            getActivityStackTokenMethod.isPublic &&
+                getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
+        }
+
+    @Suppress("newApi") // Suppress lint check for WindowMetrics
+    private fun isClassParentContainerInfoValid(): Boolean =
+        validateReflection("ParentContainerInfo is not valid") {
+            val parentContainerInfoClass = ParentContainerInfo::class.java
+            val getWindowMetricsMethod = parentContainerInfoClass.getMethod("getWindowMetrics")
+            val getConfigurationMethod = parentContainerInfoClass.getMethod("getConfiguration")
+            val getWindowLayoutInfoMethod =
+                parentContainerInfoClass.getMethod("getWindowLayoutInfo")
+            getWindowMetricsMethod.isPublic &&
+                getWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
+                getConfigurationMethod.isPublic &&
+                getConfigurationMethod.doesReturn(Configuration::class.java) &&
+                getWindowLayoutInfoMethod.isPublic &&
+                getWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java)
+        }
+
+    private fun isMethodGetParentContainerInfoValid(): Boolean =
+        validateReflection("ActivityEmbeddingComponent#getParentContainerInfo is not valid") {
+            val getParentContainerInfoMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "getParentContainerInfo",
+                    ActivityStack.Token::class.java
+                )
+            getParentContainerInfoMethod.isPublic &&
+                getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java)
+        }
+
+    private fun isMethodSetActivityStackAttributesCalculatorValid(): Boolean =
+        validateReflection("setActivityStackAttributesCalculator is not valid") {
+            val setActivityStackAttributesCalculatorMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "setActivityStackAttributesCalculator",
+                    Function::class.java
+                )
+            setActivityStackAttributesCalculatorMethod.isPublic
+        }
+
+    private fun isMethodClearActivityStackAttributesCalculatorValid(): Boolean =
+        validateReflection("clearActivityStackAttributesCalculator is not valid") {
+            val setActivityStackAttributesCalculatorMethod =
+                activityEmbeddingComponentClass.getMethod("clearActivityStackAttributesCalculator")
+            setActivityStackAttributesCalculatorMethod.isPublic
+        }
+
+    private fun isMethodUpdateActivityStackAttributesValid(): Boolean =
+        validateReflection("updateActivityStackAttributes is not valid") {
+            val updateActivityStackAttributesMethod =
+                activityEmbeddingComponentClass.getMethod(
+                    "updateActivityStackAttributes",
+                    ActivityStack.Token::class.java,
+                    ActivityStackAttributes::class.java
+                )
+            updateActivityStackAttributesMethod.isPublic
+        }
+
+    private fun isClassActivityStackAttributesValid(): Boolean =
+        validateReflection("Class ActivityStackAttributes is not valid") {
+            val activityStackAttributesClass = ActivityStackAttributes::class.java
+            val getRelativeBoundsMethod =
+                activityStackAttributesClass.getMethod("getRelativeBounds")
+            val getWindowAttributesMethod =
+                activityStackAttributesClass.getMethod("getWindowAttributes")
+            getRelativeBoundsMethod.isPublic &&
+                getRelativeBoundsMethod.doesReturn(Rect::class.java) &&
+                getWindowAttributesMethod.isPublic &&
+                getWindowAttributesMethod.doesReturn(WindowAttributes::class.java)
+        }
+
+    private fun isClassActivityStackAttributesBuilderValid(): Boolean =
+        validateReflection("Class ActivityStackAttributes.Builder is not valid") {
+            val activityStackAttributesBuilderClass = ActivityStackAttributes.Builder::class.java
+            val activityStackAttributesBuilderConstructor =
+                activityStackAttributesBuilderClass.getDeclaredConstructor()
+            val setRelativeBoundsMethod =
+                activityStackAttributesBuilderClass.getMethod("setRelativeBounds", Rect::class.java)
+            val setWindowAttributesMethod =
+                activityStackAttributesBuilderClass.getMethod(
+                    "setWindowAttributes",
+                    WindowAttributes::class.java
+                )
+            activityStackAttributesBuilderConstructor.isPublic &&
+                setRelativeBoundsMethod.isPublic &&
+                setRelativeBoundsMethod.doesReturn(ActivityStackAttributes.Builder::class.java) &&
+                setWindowAttributesMethod.isPublic &&
+                setWindowAttributesMethod.doesReturn(ActivityStackAttributes.Builder::class.java)
+        }
+
+    private fun isClassActivityStackAttributesCalculatorParamsValid(): Boolean =
+        validateReflection("Class ActivityStackAttributesCalculatorParams is not valid") {
+            val activityStackAttributesCalculatorParamsClass =
+                ActivityStackAttributesCalculatorParams::class.java
+            val getParentContainerInfoMethod =
+                activityStackAttributesCalculatorParamsClass.getMethod("getParentContainerInfo")
+            val getActivityStackTagMethod =
+                activityStackAttributesCalculatorParamsClass.getMethod("getActivityStackTag")
+            val getLaunchOptionsMethod =
+                activityStackAttributesCalculatorParamsClass.getMethod("getLaunchOptions")
+            getParentContainerInfoMethod.isPublic &&
+                getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java) &&
+                getActivityStackTagMethod.isPublic &&
+                getActivityStackTagMethod.doesReturn(String::class.java) &&
+                getLaunchOptionsMethod.isPublic &&
+                getLaunchOptionsMethod.doesReturn(Bundle::class.java)
         }
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
index 5b79c82..2d63e8a 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -19,8 +19,7 @@
 import android.annotation.SuppressLint
 import androidx.annotation.FloatRange
 import androidx.annotation.IntRange
-import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowSdkExtensions
 import androidx.window.core.SpecificationComputer.Companion.startSpecification
 import androidx.window.core.VerificationMode
@@ -35,37 +34,77 @@
  * - Layout direction &mdash; Specifies whether the parent window is split vertically or
  *   horizontally and in which direction the primary and secondary containers are respectively
  *   positioned (left to right, right to left, top to bottom, and so forth)
- * - Animation background color &mdash; The color of the background during animation of the split
- *   involving this `SplitAttributes` object if the animation requires a background
+ * - Animation params &mdash; The parameters for the animation of the split involving this
+ *   `SplitAttributes` object
+ * - Divider attributes &mdash; Specifies whether a divider is needed between the split containers
+ *   and the properties of the divider, including the color, the width, whether the divider is
+ *   draggable, etc.
  *
  * Attributes can be configured by:
  * - Setting the default `SplitAttributes` using [SplitPairRule.Builder.setDefaultSplitAttributes]
  *   or [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
- * - Setting `splitRatio`, `splitLayoutDirection`, and `animationBackgroundColor` attributes in
+ * - Setting `splitRatio`, `splitLayoutDirection`, and `animationParams` attributes in
  *   `<SplitPairRule>` or `<SplitPlaceholderRule>` tags in an XML configuration file. The attributes
- *   are parsed as [SplitType], [LayoutDirection], and [BackgroundColor], respectively. Note that
- *   [SplitType.HingeSplitType] is not supported XML format.
- * - Set `SplitAttributes` calculation function by [SplitController.setSplitAttributesCalculator] to
- *   customize the `SplitAttributes` for a given device and window state.
+ *   are parsed as [SplitType], [LayoutDirection], and [EmbeddingAnimationParams], respectively.
+ *   Note that [SplitType.HingeSplitType] is not supported XML format.
+ * - Using [SplitAttributesCalculator.computeSplitAttributesForParams] to customize the
+ *   `SplitAttributes` for a given device and window state.
  *
+ * @property splitType The split type attribute. Defaults to an equal split of the parent window for
+ *   the primary and secondary containers.
+ * @property layoutDirection The layout direction of the parent window split. The default is based
+ *   on locale value.
+ * @property animationParams The animation params to specify the animation transitions of the split
+ *   involving this `SplitAttributes` object. The default is to use the current theme window
+ *   background color and system transitions.
+ * @property dividerAttributes The [DividerAttributes] for this split. Defaults to
+ *   [DividerAttributes.NO_DIVIDER], which means no divider is requested.
  * @see SplitAttributes.SplitType
  * @see SplitAttributes.LayoutDirection
+ * @see EmbeddingAnimationParams
  */
 class SplitAttributes
-@RestrictTo(LIBRARY_GROUP)
+@JvmOverloads
 constructor(
-
-    /**
-     * The split type attribute. Defaults to an equal split of the parent window for the primary and
-     * secondary containers.
-     */
     val splitType: SplitType = SPLIT_TYPE_EQUAL,
-
-    /**
-     * The layout direction attribute for the parent window split. The default is based on locale.
-     */
     val layoutDirection: LayoutDirection = LOCALE,
+    val animationParams: EmbeddingAnimationParams = EmbeddingAnimationParams.Builder().build(),
+    val dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER,
 ) {
+    @Deprecated(
+        message = "Use default constructor with [animationParams] instead",
+    )
+    constructor(
+        splitType: SplitType,
+        layoutDirection: LayoutDirection,
+        animationBackground: EmbeddingAnimationBackground,
+        dividerAttributes: DividerAttributes,
+    ) : this(
+        splitType,
+        layoutDirection,
+        EmbeddingAnimationParams.Builder().setAnimationBackground(animationBackground).build(),
+        dividerAttributes
+    ) {}
+
+    @Deprecated(
+        message = "Use default constructor with [animationParams] instead",
+    )
+    constructor(
+        splitType: SplitType,
+        layoutDirection: LayoutDirection,
+        animationBackground: EmbeddingAnimationBackground
+    ) : this(
+        splitType,
+        layoutDirection,
+        EmbeddingAnimationParams.Builder().setAnimationBackground(animationBackground).build()
+    ) {}
+
+    @Deprecated(
+        message = "Use [animationParams] instead",
+        replaceWith = ReplaceWith("this.animationParams.animationBackground")
+    )
+    val animationBackground: EmbeddingAnimationBackground
+        get() = this.animationParams.animationBackground
 
     /**
      * The type of parent window split, which defines the proportion of the parent window occupied
@@ -333,6 +372,8 @@
     override fun hashCode(): Int {
         var result = splitType.hashCode()
         result = result * 31 + layoutDirection.hashCode()
+        result = result * 31 + animationParams.hashCode()
+        result = result * 31 + dividerAttributes.hashCode()
         return result
     }
 
@@ -345,7 +386,10 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is SplitAttributes) return false
-        return splitType == other.splitType && layoutDirection == other.layoutDirection
+        return splitType == other.splitType &&
+            layoutDirection == other.layoutDirection &&
+            animationParams == other.animationParams &&
+            dividerAttributes == other.dividerAttributes
     }
 
     /**
@@ -355,17 +399,23 @@
      */
     override fun toString(): String =
         "${SplitAttributes::class.java.simpleName}:" +
-            "{splitType=$splitType, layoutDir=$layoutDirection }"
+            "{splitType=$splitType, layoutDir=$layoutDirection, " +
+            "animationParams=$animationParams, " +
+            "dividerAttributes=$dividerAttributes }"
 
     /**
      * Builder for creating an instance of [SplitAttributes].
      * - The default split type is an equal split between primary and secondary containers.
      * - The default layout direction is based on locale.
-     * - The default animation background color is to use the current theme window background color.
+     * - The default animation params is to use the current theme window background color and system
+     *   transitions.
+     * - The default divider attributes is not to use divider.
      */
     class Builder {
         private var splitType = SPLIT_TYPE_EQUAL
         private var layoutDirection = LOCALE
+        private var animationParams = EmbeddingAnimationParams.Builder().build()
+        private var dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER
 
         /**
          * Sets the split type attribute.
@@ -391,12 +441,44 @@
             this.layoutDirection = layoutDirection
         }
 
+        /** @deprecated Use [setAnimationParams] instead. */
+        @Deprecated(message = "Use [setAnimationParams] instead")
+        @RequiresWindowSdkExtension(5)
+        fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder = apply {
+            this.animationParams =
+                EmbeddingAnimationParams.Builder().setAnimationBackground(background).build()
+        }
+
         /**
-         * Builds a `SplitAttributes` instance with the attributes specified by [setSplitType] and
-         * [setLayoutDirection].
+         * Sets the animation params to use during animation of the split involving this
+         * `SplitAttributes` object if the animation requires a background color or non-default
+         * transitions.
+         *
+         * [EmbeddingAnimationParams] can be supported only if the Window Extensions version of the
+         * target device is equals or higher than required API level. Otherwise, it would be no-op
+         * when setting the [EmbeddingAnimationParams] on a target device that has lower API level.
+         *
+         * @param params the animation params.
+         * @return this `Builder`.
+         */
+        @RequiresWindowSdkExtension(7)
+        fun setAnimationParams(params: EmbeddingAnimationParams): Builder = apply {
+            this.animationParams = params
+        }
+
+        /** Sets the [DividerAttributes]. */
+        @RequiresWindowSdkExtension(6)
+        fun setDividerAttributes(dividerAttributes: DividerAttributes): Builder = apply {
+            this.dividerAttributes = dividerAttributes
+        }
+
+        /**
+         * Builds a `SplitAttributes` instance with the attributes specified by [setSplitType],
+         * [setLayoutDirection], and [setAnimationParams].
          *
          * @return The new `SplitAttributes` instance.
          */
-        fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)
+        fun build(): SplitAttributes =
+            SplitAttributes(splitType, layoutDirection, animationParams, dividerAttributes)
     }
 }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index fc28d14e..91a498b 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -22,7 +22,6 @@
 import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowProperties
 import androidx.window.WindowSdkExtensions
-import androidx.window.core.ExperimentalWindowApi
 import androidx.window.layout.WindowMetrics
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -80,6 +79,56 @@
         get() = embeddingBackend.splitSupportStatus
 
     /**
+     * Pins the top-most [ActivityStack] to keep the stack of the Activities to be always positioned
+     * on top. The rest of the activities in the Task will be split with the pinned [ActivityStack].
+     * The pinned [ActivityStack] would also have isolated activity navigation in which only the
+     * activities that are started from the pinned [ActivityStack] can be added on top of the
+     * [ActivityStack].
+     *
+     * The pinned [ActivityStack] is unpinned whenever the pinned [ActivityStack] is expanded. Use
+     * [SplitPinRule.Builder.setSticky] if the same [ActivityStack] should be pinned again whenever
+     * the [ActivityStack] is on top and split with another [ActivityStack] again.
+     *
+     * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than or
+     * equal to 5.
+     *
+     * @param taskId The id of the Task that top [ActivityStack] should be pinned.
+     * @param splitPinRule The SplitRule that specifies how the top [ActivityStack] should be split
+     *   with others.
+     * @return Returns `true` if the top [ActivityStack] is successfully pinned. Otherwise, `false`.
+     *   Few examples are:
+     *     1. There's no [ActivityStack].
+     *     2. There is already an existing pinned [ActivityStack].
+     *     3. There's no other [ActivityStack] to split with the top [ActivityStack].
+     *
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 5.
+     */
+    @RequiresWindowSdkExtension(5)
+    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
+        return embeddingBackend.pinTopActivityStack(taskId, splitPinRule)
+    }
+
+    /**
+     * Unpins the pinned [ActivityStack]. The [ActivityStack] will still be the top-most
+     * [ActivityStack] right after unpinned, and the [ActivityStack] could be expanded or continue
+     * to be split with the next top [ActivityStack] if the current state matches any of the
+     * existing [SplitPairRule]. It is a no-op call if the task does not have a pinned
+     * [ActivityStack].
+     *
+     * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than or
+     * equal to 5.
+     *
+     * @param taskId The id of the Task that top [ActivityStack] should be unpinned.
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 5.
+     */
+    @RequiresWindowSdkExtension(5)
+    fun unpinTopActivityStack(taskId: Int) {
+        embeddingBackend.unpinTopActivityStack(taskId)
+    }
+
+    /**
      * Sets or replaces the previously registered [SplitAttributes] calculator.
      *
      * **Note** that it's callers' responsibility to check if this API is supported by checking
@@ -141,27 +190,6 @@
     }
 
     /**
-     * Triggers a [SplitAttributes] update callback for the current topmost and visible split layout
-     * if there is one. This method can be used when a change to the split presentation originates
-     * from an application state change. Changes that are driven by parent window changes or new
-     * activity starts invoke the callback provided in [setSplitAttributesCalculator] automatically
-     * without the need to call this function.
-     *
-     * The top [SplitInfo] is usually the last element of [SplitInfo] list which was received from
-     * the callback registered in [splitInfoList].
-     *
-     * The call will be ignored if there is no visible split.
-     *
-     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
-     *   than 3.
-     */
-    @ExperimentalWindowApi
-    @RequiresWindowSdkExtension(3)
-    fun invalidateTopVisibleSplitAttributes() {
-        embeddingBackend.invalidateTopVisibleSplitAttributes()
-    }
-
-    /**
      * Updates the [SplitAttributes] of a split pair. This is an alternative to using a split
      * attributes calculator callback set in [setSplitAttributesCalculator], useful when apps only
      * need to update the splits in a few cases proactively but rely on the default split attributes
@@ -176,15 +204,15 @@
      * - A new Activity being launched.
      * - A window or device state updates (e,g. due to screen rotation or folding state update).
      *
-     * In most cases it is suggested to use [invalidateTopVisibleSplitAttributes] if
-     * [SplitAttributes] calculator callback is used.
+     * In most cases it is suggested to use
+     * [ActivityEmbeddingController.invalidateTopVisibleActivityStacks] if a calculator has been set
+     * through [setSplitAttributesCalculator].
      *
      * @param splitInfo the split pair to update
      * @param splitAttributes the [SplitAttributes] to be applied
      * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
      *   than 3.
      */
-    @ExperimentalWindowApi
     @RequiresWindowSdkExtension(3)
     fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
         embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
index 0a8b4dc..40fa692 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -19,21 +19,89 @@
 import android.app.Activity
 import android.os.IBinder
 import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.WindowSdkExtensions
+import androidx.window.extensions.embedding.SplitInfo.Token
 
 /** Describes a split pair of two containers with activities. */
+@Suppress("Deprecation") // To compat with device with version 3 and 4.
 class SplitInfo
-@RestrictTo(LIBRARY_GROUP)
-constructor(
+private constructor(
     /** The [ActivityStack] representing the primary split container. */
     val primaryActivityStack: ActivityStack,
     /** The [ActivityStack] representing the secondary split container. */
     val secondaryActivityStack: ActivityStack,
     /** The [SplitAttributes] of this split pair. */
     val splitAttributes: SplitAttributes,
+    @Deprecated(
+        message = "Use [token] instead",
+        replaceWith =
+            ReplaceWith(
+                expression = "SplitInfo.token",
+                imports = arrayOf("androidx.window.embedding.SplitInfo"),
+            )
+    )
+    private val binder: IBinder?,
     /** A token uniquely identifying this `SplitInfo`. */
-    internal val token: IBinder,
+    private val token: Token?,
 ) {
+    @RequiresWindowSdkExtension(5)
+    internal constructor(
+        primaryActivityStack: ActivityStack,
+        secondaryActivityStack: ActivityStack,
+        splitAttributes: SplitAttributes,
+        token: Token,
+    ) : this(primaryActivityStack, secondaryActivityStack, splitAttributes, binder = null, token)
+
+    /** Creates SplitInfo for [WindowSdkExtensions.extensionVersion] 3 and 4. */
+    @RequiresWindowSdkExtension(3)
+    internal constructor(
+        primaryActivityStack: ActivityStack,
+        secondaryActivityStack: ActivityStack,
+        splitAttributes: SplitAttributes,
+        binder: IBinder,
+    ) : this(
+        primaryActivityStack,
+        secondaryActivityStack,
+        splitAttributes,
+        binder,
+        token = null,
+    ) {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
+    }
+
+    /**
+     * Creates SplitInfo ONLY for testing.
+     *
+     * @param primaryActivityStack the [ActivityStack] representing the primary split container.
+     * @param secondaryActivityStack the [ActivityStack] representing the secondary split container.
+     * @param splitAttributes the [SplitAttributes] of this split pair.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    constructor(
+        primaryActivityStack: ActivityStack,
+        secondaryActivityStack: ActivityStack,
+        splitAttributes: SplitAttributes,
+    ) : this(
+        primaryActivityStack,
+        secondaryActivityStack,
+        splitAttributes,
+        binder = null,
+        token = null,
+    )
+
+    @RequiresWindowSdkExtension(3)
+    internal fun getBinder(): IBinder = let {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
+        requireNotNull(binder)
+    }
+
+    @RequiresWindowSdkExtension(5)
+    internal fun getToken(): Token = let {
+        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
+        requireNotNull(token)
+    }
+
     /**
      * Whether the [primaryActivityStack] or the [secondaryActivityStack] in this [SplitInfo]
      * contains the [activity].
@@ -50,6 +118,7 @@
         if (secondaryActivityStack != other.secondaryActivityStack) return false
         if (splitAttributes != other.splitAttributes) return false
         if (token != other.token) return false
+        if (binder != other.binder) return false
 
         return true
     }
@@ -59,6 +128,7 @@
         result = 31 * result + secondaryActivityStack.hashCode()
         result = 31 * result + splitAttributes.hashCode()
         result = 31 * result + token.hashCode()
+        result = 31 * result + binder.hashCode()
         return result
     }
 
@@ -68,7 +138,12 @@
             append("primaryActivityStack=$primaryActivityStack, ")
             append("secondaryActivityStack=$secondaryActivityStack, ")
             append("splitAttributes=$splitAttributes, ")
-            append("token=$token")
+            if (token != null) {
+                append("token=$token")
+            }
+            if (binder != null) {
+                append("binder=$binder")
+            }
             append("}")
         }
     }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
new file mode 100644
index 0000000..73c45cf
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import androidx.annotation.IntRange
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT
+
+/**
+ * Split configuration rules for pinning an [ActivityStack]. Define how the pinned [ActivityStack]
+ * should be displayed side-by-side with the other [ActivityStack].
+ */
+class SplitPinRule
+internal constructor(
+    /** A unique string to identify this [SplitPinRule]. */
+    tag: String? = null,
+    /** The default [SplitAttributes] to apply on the pin container and the paired container. */
+    defaultSplitAttributes: SplitAttributes,
+    /**
+     * Whether this rule should be sticky. If the value is `false`, this rule be removed whenever
+     * the pinned [ActivityStack] is unpinned. Set to `true` if the rule should be applied whenever
+     * once again possible (e.g. the host Task bounds satisfies the size and aspect ratio
+     * requirements).
+     */
+    val isSticky: Boolean,
+    @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+    @IntRange(from = 0) minHeightDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+    @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+    maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
+    maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+) :
+    SplitRule(
+        tag,
+        minWidthDp,
+        minHeightDp,
+        minSmallestWidthDp,
+        maxAspectRatioInPortrait,
+        maxAspectRatioInLandscape,
+        defaultSplitAttributes
+    ) {
+
+    /** Builder for [SplitPinRule]. */
+    class Builder {
+        private var tag: String? = null
+        @IntRange(from = 0) private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+        @IntRange(from = 0) private var minHeightDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+        @IntRange(from = 0) private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+        private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+        private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+        private var defaultSplitAttributes = SplitAttributes.Builder().build()
+        private var isSticky: Boolean = false
+
+        /**
+         * Sets the smallest value of width of the parent window when the split should be used, in
+         * DP. When the window size is smaller than requested here, activities in the secondary
+         * container will be stacked on top of the activities in the primary one, completely
+         * overlapping them.
+         *
+         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+         *
+         * @param minWidthDp the smallest value of width of the parent window when the split should
+         *   be used, in DP.
+         */
+        fun setMinWidthDp(@IntRange(from = 0) minWidthDp: Int): Builder = apply {
+            this.minWidthDp = minWidthDp
+        }
+
+        /**
+         * Sets the smallest value of height of the parent task window when the split should be
+         * used, in DP. When the window size is smaller than requested here, activities in the
+         * secondary container will be stacked on top of the activities in the primary one,
+         * completely overlapping them.
+         *
+         * It is useful if it's necessary to split the parent window horizontally for this
+         * [SplitPinRule].
+         *
+         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+         *
+         * @param minHeightDp the smallest value of height of the parent task window when the split
+         *   should be used, in DP.
+         * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+         * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+         */
+        fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder = apply {
+            this.minHeightDp = minHeightDp
+        }
+
+        /**
+         * Sets the smallest value of the smallest possible width of the parent window in any
+         * rotation when the split should be used, in DP. When the window size is smaller than
+         * requested here, activities in the secondary container will be stacked on top of the
+         * activities in the primary one, completely overlapping them.
+         *
+         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
+         *
+         * @param minSmallestWidthDp the smallest value of the smallest possible width of the parent
+         *   window in any rotation when the split should be used, in DP.
+         */
+        fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder = apply {
+            this.minSmallestWidthDp = minSmallestWidthDp
+        }
+
+        /**
+         * Sets the largest value of the aspect ratio, expressed as `height / width` in decimal
+         * form, of the parent window bounds in portrait when the split should be used. When the
+         * window aspect ratio is greater than requested here, activities in the secondary container
+         * will be stacked on top of the activities in the primary one, completely overlapping them.
+         *
+         * This value is only used when the parent window is in portrait (height >= width).
+         *
+         * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is
+         * the recommend value to only allow split when the parent window is not too stretched in
+         * portrait.
+         *
+         * @param aspectRatio the largest value of the aspect ratio, expressed as `height / width`
+         *   in decimal form, of the parent window bounds in portrait when the split should be used.
+         * @see EmbeddingAspectRatio.ratio
+         * @see EmbeddingAspectRatio.ALWAYS_ALLOW
+         * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
+         */
+        fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder = apply {
+            this.maxAspectRatioInPortrait = aspectRatio
+        }
+
+        /**
+         * Sets the largest value of the aspect ratio, expressed as `width / height` in decimal
+         * form, of the parent window bounds in landscape when the split should be used. When the
+         * window aspect ratio is greater than requested here, activities in the secondary container
+         * will be stacked on top of the activities in the primary one, completely overlapping them.
+         *
+         * This value is only used when the parent window is in landscape (width > height).
+         *
+         * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which
+         * is the recommend value to always allow split when the parent window is in landscape.
+         *
+         * @param aspectRatio the largest value of the aspect ratio, expressed as `width / height`
+         *   in decimal form, of the parent window bounds in landscape when the split should be
+         *   used.
+         * @see EmbeddingAspectRatio.ratio
+         * @see EmbeddingAspectRatio.ALWAYS_ALLOW
+         * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
+         */
+        fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder = apply {
+            this.maxAspectRatioInLandscape = aspectRatio
+        }
+
+        /**
+         * Sets the default [SplitAttributes] to apply on the activity containers pair when the host
+         * task bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp],
+         * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.
+         *
+         * @param defaultSplitAttributes the default [SplitAttributes] to apply on the activity
+         *   containers pair when the host task bounds satisfy all the rule requirements.
+         */
+        fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder = apply {
+            this.defaultSplitAttributes = defaultSplitAttributes
+        }
+
+        /**
+         * Sets a unique string to identify this [SplitPinRule], which defaults to `null`. The
+         * suggested usage is to set the tag to be able to differentiate between different rules in
+         * the [SplitAttributesCalculatorParams.splitRuleTag].
+         *
+         * @param tag unique string to identify this [SplitPinRule].
+         */
+        fun setTag(tag: String?): Builder = apply { this.tag = tag }
+
+        /**
+         * Sets this rule to be sticky.
+         *
+         * @param isSticky whether to be a sticky rule.
+         * @see isSticky
+         */
+        fun setSticky(isSticky: Boolean): Builder = apply { this.isSticky = isSticky }
+
+        /**
+         * Builds a [SplitPinRule] instance.
+         *
+         * @return The new [SplitPinRule] instance.
+         */
+        fun build() =
+            SplitPinRule(
+                tag,
+                defaultSplitAttributes,
+                isSticky,
+                minWidthDp,
+                minHeightDp,
+                minSmallestWidthDp,
+                maxAspectRatioInPortrait,
+                maxAspectRatioInLandscape
+            )
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SplitPinRule) return false
+
+        if (!super.equals(other)) return false
+        if (isSticky != other.isSticky) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + isSticky.hashCode()
+        return result
+    }
+
+    override fun toString(): String =
+        "${SplitPinRule::class.java.simpleName}{" +
+            "tag=$tag" +
+            ", defaultSplitAttributes=$defaultSplitAttributes" +
+            ", isSticky=$isSticky" +
+            "}"
+}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index adc8732..393862c 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -129,6 +129,8 @@
      * The default [SplitAttributes] to apply on the activity containers pair when the host task
      * bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp], [maxAspectRatioInPortrait]
      * and [maxAspectRatioInLandscape] requirements.
+     *
+     * It is set to split the host parent task vertically and equally by default.
      */
     val defaultSplitAttributes: SplitAttributes,
 ) : EmbeddingRule(tag) {
diff --git a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
index 5ad6e24..75f172d 100644
--- a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
@@ -30,10 +30,13 @@
 import androidx.window.reflection.ReflectionUtils.doesReturn
 import androidx.window.reflection.ReflectionUtils.isPublic
 import androidx.window.reflection.ReflectionUtils.validateReflection
+import androidx.window.reflection.WindowExtensionsConstants.DISPLAY_FOLD_FEATURE_CLASS
 import androidx.window.reflection.WindowExtensionsConstants.FOLDING_FEATURE_CLASS
 import androidx.window.reflection.WindowExtensionsConstants.JAVA_CONSUMER
+import androidx.window.reflection.WindowExtensionsConstants.SUPPORTED_WINDOW_FEATURES_CLASS
 import androidx.window.reflection.WindowExtensionsConstants.WINDOW_CONSUMER
 import androidx.window.reflection.WindowExtensionsConstants.WINDOW_LAYOUT_COMPONENT_CLASS
+import java.lang.reflect.ParameterizedType
 
 /**
  * Reflection Guard for [WindowLayoutComponent]. This will go through the [WindowLayoutComponent]'s
@@ -63,13 +66,12 @@
         if (!isWindowLayoutComponentAccessible()) {
             return false
         }
-        // TODO(b/267831038): can fallback to VendorApiLevel1 when level2 is not match
-        //  but level 1 is matched
-        return when (ExtensionsUtil.safeVendorApiLevel) {
-            1 -> hasValidVendorApiLevel1()
-            in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
-            // TODO(b/267956499): add hasValidVendorApiLevel3
-            else -> false
+        val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
+        return when {
+            vendorApiLevel < 1 -> false
+            vendorApiLevel == 1 -> hasValidVendorApiLevel1()
+            vendorApiLevel < 5 -> hasValidVendorApiLevel2()
+            else -> hasValidVendorApiLevel6()
         }
     }
 
@@ -100,6 +102,14 @@
         return hasValidVendorApiLevel1() && isMethodWindowLayoutInfoListenerWindowConsumerValid()
     }
 
+    @VisibleForTesting
+    internal fun hasValidVendorApiLevel6(): Boolean {
+        return hasValidVendorApiLevel2() &&
+            isDisplayFoldFeatureValid() &&
+            isSupportedWindowFeaturesValid() &&
+            isGetSupportedWindowFeaturesValid()
+    }
+
     private fun isWindowLayoutProviderValid(): Boolean {
         return validateReflection("WindowExtensions#getWindowLayoutComponent is not valid") {
             val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
@@ -167,6 +177,63 @@
         }
     }
 
+    private fun isDisplayFoldFeatureValid(): Boolean {
+        return validateReflection("DisplayFoldFeature is not valid") {
+            val displayFoldFeatureClass = displayFoldFeatureClass
+
+            val getTypeMethod = displayFoldFeatureClass.getMethod("getType")
+            val hasPropertyMethod =
+                displayFoldFeatureClass.getMethod("hasProperty", Int::class.java)
+            val hasPropertiesMethod =
+                displayFoldFeatureClass.getMethod("hasProperties", IntArray::class.java)
+
+            getTypeMethod.isPublic &&
+                getTypeMethod.doesReturn(Int::class.java) &&
+                hasPropertyMethod.isPublic &&
+                hasPropertyMethod.doesReturn(Boolean::class.java) &&
+                hasPropertiesMethod.isPublic &&
+                hasPropertiesMethod.doesReturn(Boolean::class.java)
+        }
+    }
+
+    private fun isSupportedWindowFeaturesValid(): Boolean {
+        return validateReflection("SupportedWindowFeatures is not valid") {
+            val supportedWindowFeaturesClass = supportedWindowFeaturesClass
+
+            val getDisplayFoldFeaturesMethod =
+                supportedWindowFeaturesClass.getMethod("getDisplayFoldFeatures")
+            val returnTypeGeneric =
+                (getDisplayFoldFeaturesMethod.genericReturnType as ParameterizedType)
+                    .actualTypeArguments[0]
+                    as Class<*>
+
+            getDisplayFoldFeaturesMethod.isPublic &&
+                getDisplayFoldFeaturesMethod.doesReturn(List::class.java) &&
+                returnTypeGeneric == displayFoldFeatureClass
+        }
+    }
+
+    private fun isGetSupportedWindowFeaturesValid(): Boolean {
+        return validateReflection("WindowLayoutComponent#getSupportedWindowFeatures is not valid") {
+            val windowLayoutComponent = windowLayoutComponentClass
+            val getSupportedWindowFeaturesMethod =
+                windowLayoutComponent.getMethod("getSupportedWindowFeatures")
+
+            getSupportedWindowFeaturesMethod.isPublic &&
+                getSupportedWindowFeaturesMethod.doesReturn(supportedWindowFeaturesClass)
+        }
+    }
+
+    private val displayFoldFeatureClass: Class<*>
+        get() {
+            return loader.loadClass(DISPLAY_FOLD_FEATURE_CLASS)
+        }
+
+    private val supportedWindowFeaturesClass: Class<*>
+        get() {
+            return loader.loadClass(SUPPORTED_WINDOW_FEATURES_CLASS)
+        }
+
     private val foldingFeatureClass: Class<*>
         get() {
             return loader.loadClass(FOLDING_FEATURE_CLASS)
diff --git a/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
new file mode 100644
index 0000000..0e29629
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout
+
+/** A class to represent a posture that the device supports. */
+class SupportedPosture internal constructor(private val rawValue: Int) {
+
+    override fun toString(): String {
+        return when (this) {
+            TABLETOP -> "TABLETOP"
+            else -> "UNKNOWN"
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null) return false
+        if (other::class != SupportedPosture::class) return false
+
+        other as SupportedPosture
+
+        return rawValue == other.rawValue
+    }
+
+    override fun hashCode(): Int {
+        return rawValue
+    }
+
+    companion object {
+        /** The posture where there is a single fold in the half-opened state. */
+        @JvmField val TABLETOP: SupportedPosture = SupportedPosture(0)
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
index ffb3a1f..d4b7534 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
 import androidx.annotation.UiContext
+import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
 import androidx.window.layout.adapter.WindowBackend
@@ -96,6 +97,23 @@
      */
     fun windowLayoutInfo(activity: Activity): Flow<WindowLayoutInfo>
 
+    /**
+     * Returns the [List] of [SupportedPosture] values. This value will not change during runtime.
+     * These values are for determining if the device supports the given [SupportedPosture] but does
+     * not mean the device is in the given [SupportedPosture]. Use [windowLayoutInfo] to determine
+     * the current state of the [DisplayFeature]'s on the device.
+     *
+     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less
+     *   than 6.
+     * @throws NotImplementedError if a derived test class does not override this method.
+     * @see windowLayoutInfo
+     */
+    @RequiresWindowSdkExtension(version = 6)
+    val supportedPostures: List<SupportedPosture>
+        get() {
+            throw NotImplementedError("Method was not implemented.")
+        }
+
     companion object {
 
         private val DEBUG = false
@@ -134,7 +152,12 @@
         @JvmStatic
         fun getOrCreate(context: Context): WindowInfoTracker {
             val backend = extensionBackend ?: SidecarWindowBackend.getInstance(context)
-            val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, backend)
+            val repo =
+                WindowInfoTrackerImpl(
+                    WindowMetricsCalculatorCompat(),
+                    backend,
+                    WindowSdkExtensions.getInstance()
+                )
             return decorator.decorate(repo)
         }
 
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
index e6702d0..308042f 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import androidx.annotation.UiContext
 import androidx.core.util.Consumer
+import androidx.window.WindowSdkExtensions
 import androidx.window.layout.adapter.WindowBackend
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.awaitClose
@@ -36,7 +37,8 @@
  */
 internal class WindowInfoTrackerImpl(
     private val windowMetricsCalculator: WindowMetricsCalculator,
-    private val windowBackend: WindowBackend
+    private val windowBackend: WindowBackend,
+    private val windowSdkExtensions: WindowSdkExtensions
 ) : WindowInfoTracker {
 
     /**
@@ -61,4 +63,10 @@
             }
             .flowOn(Dispatchers.Main)
     }
+
+    override val supportedPostures: List<SupportedPosture>
+        get() {
+            windowSdkExtensions.requireExtensionVersion(6)
+            return windowBackend.supportedPostures
+        }
 }
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
index 7aaa8e1..6707353 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
@@ -17,6 +17,7 @@
 
 import android.graphics.Rect
 import android.os.Build.VERSION_CODES
+import android.util.DisplayMetrics
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.core.view.WindowInsetsCompat
@@ -34,15 +35,23 @@
 class WindowMetrics
 internal constructor(
     private val _bounds: Bounds,
-    private val _windowInsetsCompat: WindowInsetsCompat
+    private val _windowInsetsCompat: WindowInsetsCompat,
+
+    /**
+     * Returns the logical density of the display this window is in.
+     *
+     * @see [DisplayMetrics.density]
+     */
+    val density: Float
 ) {
 
     /** An internal constructor for [WindowMetrics] */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     constructor(
         bounds: Rect,
-        insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build()
-    ) : this(Bounds(bounds), insets)
+        insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build(),
+        density: Float
+    ) : this(Bounds(bounds), insets, density)
 
     /**
      * Returns a new [Rect] describing the bounds of the area the window occupies.
@@ -68,6 +77,7 @@
 
         if (_bounds != other._bounds) return false
         if (_windowInsetsCompat != other._windowInsetsCompat) return false
+        if (density != other.density) return false
 
         return true
     }
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
index 9630be0..04060a1 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
@@ -28,6 +28,7 @@
 import androidx.annotation.UiContext
 import androidx.core.view.WindowInsetsCompat
 import androidx.window.core.Bounds
+import androidx.window.layout.util.WindowMetricsCompatHelper
 
 /** An interface to calculate the [WindowMetrics] for an [Activity] or a [UiContext]. */
 interface WindowMetricsCalculator {
@@ -123,10 +124,11 @@
     companion object {
 
         private var decorator: (WindowMetricsCalculator) -> WindowMetricsCalculator = { it }
+        private val windowMetricsCalculatorCompat = WindowMetricsCalculatorCompat()
 
         @JvmStatic
         fun getOrCreate(): WindowMetricsCalculator {
-            return decorator(WindowMetricsCalculatorCompat)
+            return decorator(windowMetricsCalculatorCompat)
         }
 
         @JvmStatic
@@ -145,18 +147,20 @@
          * Converts [Android API WindowMetrics][AndroidWindowMetrics] to
          * [Jetpack version WindowMetrics][WindowMetrics]
          */
-        @Suppress("ClassVerificationFailure")
         @RequiresApi(Build.VERSION_CODES.R)
-        internal fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics): WindowMetrics =
-            WindowMetrics(
-                windowMetrics.bounds,
-                WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
-            )
+        internal fun translateWindowMetrics(
+            windowMetrics: AndroidWindowMetrics,
+            density: Float
+        ): WindowMetrics {
+            return WindowMetricsCompatHelper.getInstance()
+                .translateWindowMetrics(windowMetrics, density)
+        }
 
         internal fun fromDisplayMetrics(displayMetrics: DisplayMetrics): WindowMetrics {
             return WindowMetrics(
                 Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
-                WindowInsetsCompat.Builder().build()
+                WindowInsetsCompat.Builder().build(),
+                displayMetrics.density
             )
         }
     }
diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
index fd77478..ffaa59a 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
@@ -15,40 +15,18 @@
  */
 package androidx.window.layout
 
-import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Point
-import android.graphics.Rect
 import android.inputmethodservice.InputMethodService
-import android.os.Build
-import android.os.Build.VERSION_CODES
-import android.util.Log
-import android.view.Display
-import android.view.DisplayCutout
-import android.view.WindowManager
-import androidx.annotation.RequiresApi
 import androidx.annotation.UiContext
-import androidx.annotation.VisibleForTesting
 import androidx.core.view.WindowInsetsCompat
-import androidx.window.core.Bounds
-import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
-import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowBounds
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
-import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowMetrics
-import androidx.window.layout.util.ContextCompatHelperApi30.maximumWindowBounds
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
-import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
-import java.lang.reflect.InvocationTargetException
+import androidx.window.layout.util.DensityCompatHelper
+import androidx.window.layout.util.WindowMetricsCompatHelper
 
 /** Helper class used to compute window metrics across Android versions. */
-internal object WindowMetricsCalculatorCompat : WindowMetricsCalculator {
-
-    private val TAG: String = WindowMetricsCalculatorCompat::class.java.simpleName
+internal class WindowMetricsCalculatorCompat(
+    private val densityCompatHelper: DensityCompatHelper = DensityCompatHelper.getInstance()
+) : WindowMetricsCalculator {
 
     /**
      * Computes the current [WindowMetrics] for a given [Context]. The context can be either an
@@ -57,33 +35,8 @@
      * @see WindowMetricsCalculator.computeCurrentWindowMetrics
      */
     override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
-        // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
-        if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
-            return currentWindowMetrics(context)
-        } else {
-            when (val unwrappedContext = unwrapUiContext(context)) {
-                is Activity -> {
-                    return computeCurrentWindowMetrics(unwrappedContext)
-                }
-                is InputMethodService -> {
-                    val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
-                    // On older SDK levels, the app and IME could show up on different displays.
-                    // However, there isn't a way for us to figure this out from the application
-                    // layer. But, this should be good enough for now given the small likelihood of
-                    // IMEs showing up on non-primary displays on these SDK levels.
-                    @Suppress("DEPRECATION")
-                    val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
-
-                    // IME occupies the whole display bounds.
-                    val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
-                    return WindowMetrics(imeBounds)
-                }
-                else -> {
-                    throw IllegalArgumentException("$context is not a UiContext")
-                }
-            }
-        }
+        return WindowMetricsCompatHelper.getInstance()
+            .currentWindowMetrics(context, densityCompatHelper)
     }
 
     /**
@@ -92,26 +45,8 @@
      * @see WindowMetricsCalculator.computeCurrentWindowMetrics
      */
     override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
-        val bounds =
-            if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
-                currentWindowBounds(activity)
-            } else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
-                computeWindowBoundsQ(activity)
-            } else if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
-                computeWindowBoundsP(activity)
-            } else if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
-                computeWindowBoundsN(activity)
-            } else {
-                computeWindowBoundsIceCreamSandwich(activity)
-            }
-        // TODO (b/233899790): compute insets for other platform versions below R
-        val windowInsetsCompat =
-            if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
-                computeWindowInsetsCompat(activity)
-            } else {
-                WindowInsetsCompat.Builder().build()
-            }
-        return WindowMetrics(Bounds(bounds), windowInsetsCompat)
+        return WindowMetricsCompatHelper.getInstance()
+            .currentWindowMetrics(activity, densityCompatHelper)
     }
 
     /**
@@ -120,7 +55,8 @@
      * @see WindowMetricsCalculator.computeMaximumWindowMetrics
      */
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
-        return computeMaximumWindowMetrics(activity as Context)
+        return WindowMetricsCompatHelper.getInstance()
+            .maximumWindowMetrics(activity, densityCompatHelper)
     }
 
     /**
@@ -129,288 +65,8 @@
      * @See WindowMetricsCalculator.computeMaximumWindowMetrics
      */
     override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
-        // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
-        val bounds =
-            if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
-                maximumWindowBounds(context)
-            } else {
-                val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-                // [WindowManager#getDefaultDisplay] is deprecated but we have this for
-                // compatibility with older versions, as we can't reliably get the display
-                // associated
-                // with a Context through public APIs either.
-                @Suppress("DEPRECATION") val display = wm.defaultDisplay
-                val displaySize = getRealSizeForDisplay(display)
-                Rect(0, 0, displaySize.x, displaySize.y)
-            }
-        // TODO (b/233899790): compute insets for other platform versions below R
-        val windowInsetsCompat =
-            if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
-                computeWindowInsetsCompat(context)
-            } else {
-                WindowInsetsCompat.Builder().build()
-            }
-        return WindowMetrics(Bounds(bounds), windowInsetsCompat)
-    }
-
-    /** Computes the window bounds for [Build.VERSION_CODES.Q]. */
-    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
-    @RequiresApi(VERSION_CODES.Q)
-    internal fun computeWindowBoundsQ(activity: Activity): Rect {
-        var bounds: Rect
-        val config = activity.resources.configuration
-        try {
-            val windowConfigField =
-                Configuration::class.java.getDeclaredField("windowConfiguration")
-            windowConfigField.isAccessible = true
-            val windowConfig = windowConfigField[config]
-            val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
-            bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
-        } catch (e: NoSuchFieldException) {
-            Log.w(TAG, e)
-            // If reflection fails for some reason default to the P implementation which still
-            // has the ability to account for display cutouts.
-            bounds = computeWindowBoundsP(activity)
-        } catch (e: NoSuchMethodException) {
-            Log.w(TAG, e)
-            bounds = computeWindowBoundsP(activity)
-        } catch (e: IllegalAccessException) {
-            Log.w(TAG, e)
-            bounds = computeWindowBoundsP(activity)
-        } catch (e: InvocationTargetException) {
-            Log.w(TAG, e)
-            bounds = computeWindowBoundsP(activity)
-        }
-        return bounds
-    }
-
-    /**
-     * Computes the window bounds for [Build.VERSION_CODES.P].
-     *
-     * NOTE: This method may result in incorrect values if the [android.content.res.Resources] value
-     * stored at 'navigation_bar_height' does not match the true navigation bar inset on the window.
-     */
-    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
-    @RequiresApi(VERSION_CODES.P)
-    internal fun computeWindowBoundsP(activity: Activity): Rect {
-        val bounds = Rect()
-        val config = activity.resources.configuration
-        try {
-            val windowConfigField =
-                Configuration::class.java.getDeclaredField("windowConfiguration")
-            windowConfigField.isAccessible = true
-            val windowConfig = windowConfigField[config]
-
-            // In multi-window mode we'll use the WindowConfiguration#mBounds property which
-            // should match the window size. Otherwise we'll use the mAppBounds property and
-            // will adjust it below.
-            if (isInMultiWindowMode(activity)) {
-                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
-                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
-            } else {
-                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
-                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
-            }
-        } catch (e: NoSuchFieldException) {
-            Log.w(TAG, e)
-            getRectSizeFromDisplay(activity, bounds)
-        } catch (e: NoSuchMethodException) {
-            Log.w(TAG, e)
-            getRectSizeFromDisplay(activity, bounds)
-        } catch (e: IllegalAccessException) {
-            Log.w(TAG, e)
-            getRectSizeFromDisplay(activity, bounds)
-        } catch (e: InvocationTargetException) {
-            Log.w(TAG, e)
-            getRectSizeFromDisplay(activity, bounds)
-        }
-        val platformWindowManager = activity.windowManager
-
-        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") val currentDisplay = platformWindowManager.defaultDisplay
-        val realDisplaySize = Point()
-        @Suppress("DEPRECATION") currentDisplay.getRealSize(realDisplaySize)
-
-        if (!isInMultiWindowMode(activity)) {
-            // The activity is not in multi-window mode. Check if the addition of the
-            // navigation bar size to mAppBounds results in the real display size and if so
-            // assume the nav bar height should be added to the result.
-            val navigationBarHeight = getNavigationBarHeight(activity)
-            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
-                bounds.bottom += navigationBarHeight
-            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
-                bounds.right += navigationBarHeight
-            } else if (bounds.left == navigationBarHeight) {
-                bounds.left = 0
-            }
-        }
-        if (
-            (bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
-                !isInMultiWindowMode(activity)
-        ) {
-            // If the corrected bounds are not the same as the display size and the activity is
-            // not in multi-window mode it is possible there are unreported cutouts inset-ing
-            // the window depending on the layoutInCutoutMode. Check for them here by getting
-            // the cutout from the display itself.
-            val displayCutout = getCutoutForDisplay(currentDisplay)
-            if (displayCutout != null) {
-                if (bounds.left == safeInsetLeft(displayCutout)) {
-                    bounds.left = 0
-                }
-                if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
-                    bounds.right += safeInsetRight(displayCutout)
-                }
-                if (bounds.top == safeInsetTop(displayCutout)) {
-                    bounds.top = 0
-                }
-                if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
-                    bounds.bottom += safeInsetBottom(displayCutout)
-                }
-            }
-        }
-        return bounds
-    }
-
-    private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
-        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
-        // [Display#getRectSize] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
-    }
-
-    /**
-     * Computes the window bounds for platforms between [Build.VERSION_CODES.N] and
-     * [Build.VERSION_CODES.O_MR1], inclusive.
-     *
-     * NOTE: This method may result in incorrect values under the following conditions:
-     * * If the activity is in multi-window mode the origin of the returned bounds will always be
-     *   anchored at (0, 0).
-     * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does not
-     *   match the true navigation bar size the returned bounds will not take into account the
-     *   navigation bar.
-     */
-    @RequiresApi(VERSION_CODES.N)
-    internal fun computeWindowBoundsN(activity: Activity): Rect {
-        val bounds = Rect()
-        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
-        // [Display#getRectSize] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
-        if (!isInMultiWindowMode(activity)) {
-            // The activity is not in multi-window mode. Check if the addition of the
-            // navigation bar size to Display#getSize() results in the real display size and
-            // if so return this value. If not, return the result of Display#getSize().
-            val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
-            val navigationBarHeight = getNavigationBarHeight(activity)
-            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
-                bounds.bottom += navigationBarHeight
-            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
-                bounds.right += navigationBarHeight
-            }
-        }
-        return bounds
-    }
-
-    /**
-     * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN] and
-     * [Build.VERSION_CODES.M], inclusive.
-     *
-     * Given that multi-window mode isn't supported before N we simply return the real display size
-     * which should match the window size of a full-screen app.
-     */
-    internal fun computeWindowBoundsIceCreamSandwich(activity: Activity): Rect {
-        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
-        // compatibility with older versions
-        @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
-        val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
-        val bounds = Rect()
-        if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
-            // [Display#getRectSize] is deprecated but we have this for
-            // compatibility with older versions
-            @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
-        } else {
-            bounds.right = realDisplaySize.x
-            bounds.bottom = realDisplaySize.y
-        }
-        return bounds
-    }
-
-    /**
-     * Returns the full (real) size of the display, in pixels, without subtracting any window decor
-     * or applying any compatibility scale factors.
-     *
-     * The size is adjusted based on the current rotation of the display.
-     *
-     * @return a point representing the real display size in pixels.
-     * @see Display.getRealSize
-     */
-    @VisibleForTesting
-    @Suppress("DEPRECATION")
-    internal fun getRealSizeForDisplay(display: Display): Point {
-        val size = Point()
-        display.getRealSize(size)
-        return size
-    }
-
-    /**
-     * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
-     *
-     * Note: This is error-prone and is **not** the recommended way to determine the size of the
-     * overlapping region between the navigation bar and a given window. The best approach is to
-     * acquire the [android.view.WindowInsets].
-     */
-    private fun getNavigationBarHeight(context: Context): Int {
-        val resources = context.resources
-        val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
-        return if (resourceId > 0) {
-            resources.getDimensionPixelSize(resourceId)
-        } else 0
-    }
-
-    /**
-     * Returns the [DisplayCutout] for the given display. Note that display cutout returned here is
-     * for the display and the insets provided are in the display coordinate system.
-     *
-     * @return the display cutout for the given display.
-     */
-    @SuppressLint("BanUncheckedReflection")
-    @RequiresApi(VERSION_CODES.P)
-    private fun getCutoutForDisplay(display: Display): DisplayCutout? {
-        var displayCutout: DisplayCutout? = null
-        try {
-            val displayInfoClass = Class.forName("android.view.DisplayInfo")
-            val displayInfoConstructor = displayInfoClass.getConstructor()
-            displayInfoConstructor.isAccessible = true
-            val displayInfo = displayInfoConstructor.newInstance()
-            val getDisplayInfoMethod =
-                display.javaClass.getDeclaredMethod("getDisplayInfo", displayInfo.javaClass)
-            getDisplayInfoMethod.isAccessible = true
-            getDisplayInfoMethod.invoke(display, displayInfo)
-            val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
-            displayCutoutField.isAccessible = true
-            val cutout = displayCutoutField[displayInfo]
-            if (cutout is DisplayCutout) {
-                displayCutout = cutout
-            }
-        } catch (e: ClassNotFoundException) {
-            Log.w(TAG, e)
-        } catch (e: NoSuchMethodException) {
-            Log.w(TAG, e)
-        } catch (e: NoSuchFieldException) {
-            Log.w(TAG, e)
-        } catch (e: IllegalAccessException) {
-            Log.w(TAG, e)
-        } catch (e: InvocationTargetException) {
-            Log.w(TAG, e)
-        } catch (e: InstantiationException) {
-            Log.w(TAG, e)
-        }
-        return displayCutout
+        return WindowMetricsCompatHelper.getInstance()
+            .maximumWindowMetrics(context, densityCompatHelper)
     }
 
     /** [ArrayList] that defines different types of sources causing window insets. */
@@ -425,17 +81,4 @@
             WindowInsetsCompat.Type.tappableElement(),
             WindowInsetsCompat.Type.displayCutout()
         )
-
-    /** Computes the current [WindowInsetsCompat] for a given [Context]. */
-    @RequiresApi(VERSION_CODES.R)
-    internal fun computeWindowInsetsCompat(@UiContext context: Context): WindowInsetsCompat {
-        val build = Build.VERSION.SDK_INT
-        val windowInsetsCompat =
-            if (build >= VERSION_CODES.R) {
-                currentWindowInsets(context)
-            } else {
-                throw Exception("Incompatible SDK version")
-            }
-        return windowInsetsCompat
-    }
 }
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
index 9b7e977..c147914 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
@@ -20,6 +20,8 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiContext
 import androidx.core.util.Consumer
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import java.util.concurrent.Executor
 
@@ -48,4 +50,11 @@
     fun hasRegisteredListeners(): Boolean {
         return false
     }
+
+    /**
+     * Returns a [List] of [SupportedPosture] for the device.
+     *
+     * @throws UnsupportedOperationException if the Window SDK version is less than 6.
+     */
+    @RequiresWindowSdkExtension(version = 6) val supportedPostures: List<SupportedPosture>
 }
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
index 9179308..9d53488 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
@@ -42,7 +42,8 @@
         fun newInstance(component: WindowLayoutComponent, adapter: ConsumerAdapter): WindowBackend {
             val safeVendorApiLevel = ExtensionsUtil.safeVendorApiLevel
             return when {
-                safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component)
+                safeVendorApiLevel >= 6 -> ExtensionWindowBackendApi6(component, adapter)
+                safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component, adapter)
                 safeVendorApiLevel == 1 -> ExtensionWindowBackendApi1(component, adapter)
                 else -> ExtensionWindowBackendApi0()
             }
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
index 39e2cfe..72b4c87 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import androidx.core.util.Consumer
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.adapter.WindowBackend
 import java.util.concurrent.Executor
@@ -35,4 +36,10 @@
     override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
         // empty implementation since there are no consumers
     }
+
+    override val supportedPostures: List<SupportedPosture>
+        get() =
+            throw UnsupportedOperationException(
+                "supportedPostures is only supported on Window SDK 6."
+            )
 }
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
index a32b465..33c53a7 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
@@ -25,14 +25,15 @@
 import androidx.window.core.ConsumerAdapter
 import androidx.window.extensions.layout.WindowLayoutComponent
 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.adapter.WindowBackend
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
 
-internal class ExtensionWindowBackendApi1(
-    private val component: WindowLayoutComponent,
+internal open class ExtensionWindowBackendApi1(
+    val component: WindowLayoutComponent,
     private val consumerAdapter: ConsumerAdapter
 ) : WindowBackend {
 
@@ -125,4 +126,7 @@
             listenerToContext.isEmpty() &&
             consumerToToken.isEmpty())
     }
+
+    override val supportedPostures: List<SupportedPosture>
+        get() = throw UnsupportedOperationException("Extensions version must be at least 6")
 }
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
index 07e5917..0a20ba9 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
@@ -22,15 +22,17 @@
 import androidx.annotation.UiContext
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.Consumer
+import androidx.window.core.ConsumerAdapter
 import androidx.window.extensions.layout.WindowLayoutComponent
 import androidx.window.layout.WindowLayoutInfo
-import androidx.window.layout.adapter.WindowBackend
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
 
-internal class ExtensionWindowBackendApi2(private val component: WindowLayoutComponent) :
-    WindowBackend {
+internal open class ExtensionWindowBackendApi2(
+    component: WindowLayoutComponent,
+    adapter: ConsumerAdapter
+) : ExtensionWindowBackendApi1(component, adapter) {
 
     private val globalLock = ReentrantLock()
 
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
new file mode 100644
index 0000000..ee9ff3c
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.adapter.extensions
+
+import androidx.window.RequiresWindowSdkExtension
+import androidx.window.core.ConsumerAdapter
+import androidx.window.extensions.layout.WindowLayoutComponent
+import androidx.window.layout.SupportedPosture
+
+@RequiresWindowSdkExtension(version = 6)
+internal class ExtensionWindowBackendApi6(
+    component: WindowLayoutComponent,
+    adapter: ConsumerAdapter
+) : ExtensionWindowBackendApi2(component, adapter) {
+    override val supportedPostures: List<SupportedPosture>
+        get() = ExtensionsWindowLayoutInfoAdapter.translate(component.supportedWindowFeatures)
+}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
index 66cdf23..3ad07c2 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
@@ -21,7 +21,9 @@
 import android.os.Build
 import androidx.annotation.UiContext
 import androidx.window.core.Bounds
+import androidx.window.extensions.layout.DisplayFoldFeature
 import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
+import androidx.window.extensions.layout.SupportedWindowFeatures
 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.FoldingFeature.State.Companion.FLAT
@@ -29,9 +31,10 @@
 import androidx.window.layout.HardwareFoldingFeature
 import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
 import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.WindowMetrics
-import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
+import androidx.window.layout.WindowMetricsCalculatorCompat
 
 internal object ExtensionsWindowLayoutInfoAdapter {
 
@@ -63,10 +66,11 @@
         @UiContext context: Context,
         info: OEMWindowLayoutInfo,
     ): WindowLayoutInfo {
+        val calculator = WindowMetricsCalculatorCompat()
         return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-            translate(computeCurrentWindowMetrics(context), info)
+            translate(calculator.computeCurrentWindowMetrics(context), info)
         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (context is Activity)) {
-            translate(computeCurrentWindowMetrics(context), info)
+            translate(calculator.computeCurrentWindowMetrics(context), info)
         } else {
             throw UnsupportedOperationException(
                 "Display Features are only supported after Q. Display features for non-Activity " +
@@ -89,6 +93,18 @@
         return WindowLayoutInfo(features)
     }
 
+    internal fun translate(features: SupportedWindowFeatures): List<SupportedPosture> {
+        val isTableTopSupported =
+            features.displayFoldFeatures.any { feature ->
+                feature.hasProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
+            }
+        return if (isTableTopSupported) {
+            listOf(SupportedPosture.TABLETOP)
+        } else {
+            emptyList()
+        }
+    }
+
     /**
      * Checks the bounds for a [FoldingFeature] within a given [WindowMetrics]. Validates the
      * following:
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
index 14a603c..31ee16f 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.Consumer
 import androidx.window.core.Version
+import androidx.window.layout.SupportedPosture
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.adapter.WindowBackend
 import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface
@@ -127,6 +128,9 @@
         }
     }
 
+    override val supportedPostures: List<SupportedPosture>
+        get() = throw UnsupportedOperationException("Must be called from extensions.")
+
     /**
      * Checks if there are no more registered callbacks left for the activity and inform extension
      * if needed.
diff --git a/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
new file mode 100644
index 0000000..3d24d92
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.Build
+import android.util.Log
+import android.view.Display
+import android.view.DisplayCutout
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
+import androidx.window.layout.util.BoundsHelper.Companion.TAG
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
+import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
+import java.lang.reflect.InvocationTargetException
+
+/** Provides compatibility behavior for calculating bounds of an activity. */
+internal interface BoundsHelper {
+
+    /** Compute the current bounds for the given [Activity]. */
+    fun currentWindowBounds(activity: Activity): Rect
+
+    fun maximumWindowBounds(@UiContext context: Context): Rect
+
+    companion object {
+
+        val TAG: String = BoundsHelper::class.java.simpleName
+
+        fun getInstance(): BoundsHelper {
+            return when {
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
+                    BoundsHelperApi30Impl
+                }
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
+                    BoundsHelperApi29Impl
+                }
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
+                    BoundsHelperApi28Impl
+                }
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
+                    BoundsHelperApi24Impl
+                }
+                else -> {
+                    BoundsHelperApi16Impl
+                }
+            }
+        }
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private object BoundsHelperApi30Impl : BoundsHelper {
+    override fun currentWindowBounds(activity: Activity): Rect {
+        val wm = activity.getSystemService(WindowManager::class.java)
+        return wm.currentWindowMetrics.bounds
+    }
+
+    override fun maximumWindowBounds(@UiContext context: Context): Rect {
+        val wm = context.getSystemService(WindowManager::class.java)
+        return wm.maximumWindowMetrics.bounds
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.Q)
+private object BoundsHelperApi29Impl : BoundsHelper {
+
+    /** Computes the window bounds for [Build.VERSION_CODES.Q]. */
+    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
+    override fun currentWindowBounds(activity: Activity): Rect {
+        var bounds: Rect
+        val config = activity.resources.configuration
+        try {
+            val windowConfigField =
+                Configuration::class.java.getDeclaredField("windowConfiguration")
+            windowConfigField.isAccessible = true
+            val windowConfig = windowConfigField[config]
+            val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
+            bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
+        } catch (e: Exception) {
+            when (e) {
+                is NoSuchFieldException,
+                is NoSuchMethodException,
+                is IllegalAccessException,
+                is InvocationTargetException -> {
+                    Log.w(TAG, e)
+                    // If reflection fails for some reason default to the P implementation which
+                    // still has the ability to account for display cutouts.
+                    bounds = BoundsHelperApi28Impl.currentWindowBounds(activity)
+                }
+                else -> throw e
+            }
+        }
+        return bounds
+    }
+
+    override fun maximumWindowBounds(@UiContext context: Context): Rect {
+        return BoundsHelperApi28Impl.maximumWindowBounds(context)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+private object BoundsHelperApi28Impl : BoundsHelper {
+
+    /**
+     * Computes the window bounds for [Build.VERSION_CODES.P].
+     *
+     * NOTE: This method may result in incorrect values if the [android.content.res.Resources] value
+     * stored at 'navigation_bar_height' does not match the true navigation bar inset on the window.
+     */
+    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
+    override fun currentWindowBounds(activity: Activity): Rect {
+        val bounds = Rect()
+        val config = activity.resources.configuration
+        try {
+            val windowConfigField =
+                Configuration::class.java.getDeclaredField("windowConfiguration")
+            windowConfigField.isAccessible = true
+            val windowConfig = windowConfigField[config]
+
+            // In multi-window mode we'll use the WindowConfiguration#mBounds property which
+            // should match the window size. Otherwise we'll use the mAppBounds property and
+            // will adjust it below.
+            if (isInMultiWindowMode(activity)) {
+                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
+                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
+            } else {
+                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
+                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
+            }
+        } catch (e: Exception) {
+            when (e) {
+                is NoSuchFieldException,
+                is NoSuchMethodException,
+                is IllegalAccessException,
+                is InvocationTargetException -> {
+                    Log.w(TAG, e)
+                    getRectSizeFromDisplay(activity, bounds)
+                }
+                else -> throw e
+            }
+        }
+
+        val platformWindowManager = activity.windowManager
+
+        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+        // compatibility with older versions
+        @Suppress("DEPRECATION") val currentDisplay = platformWindowManager.defaultDisplay
+        val realDisplaySize = Point()
+        @Suppress("DEPRECATION") currentDisplay.getRealSize(realDisplaySize)
+
+        if (!isInMultiWindowMode(activity)) {
+            // The activity is not in multi-window mode. Check if the addition of the
+            // navigation bar size to mAppBounds results in the real display size and if so
+            // assume the nav bar height should be added to the result.
+            val navigationBarHeight = getNavigationBarHeight(activity)
+            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
+                bounds.bottom += navigationBarHeight
+            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
+                bounds.right += navigationBarHeight
+            } else if (bounds.left == navigationBarHeight) {
+                bounds.left = 0
+            }
+        }
+        if (
+            (bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
+                !isInMultiWindowMode(activity)
+        ) {
+            // If the corrected bounds are not the same as the display size and the activity is
+            // not in multi-window mode it is possible there are unreported cutouts inset-ing
+            // the window depending on the layoutInCutoutMode. Check for them here by getting
+            // the cutout from the display itself.
+            val displayCutout = getCutoutForDisplay(currentDisplay)
+            if (displayCutout != null) {
+                if (bounds.left == safeInsetLeft(displayCutout)) {
+                    bounds.left = 0
+                }
+                if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
+                    bounds.right += safeInsetRight(displayCutout)
+                }
+                if (bounds.top == safeInsetTop(displayCutout)) {
+                    bounds.top = 0
+                }
+                if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
+                    bounds.bottom += safeInsetBottom(displayCutout)
+                }
+            }
+        }
+        return bounds
+    }
+
+    override fun maximumWindowBounds(@UiContext context: Context): Rect {
+        return BoundsHelperApi24Impl.maximumWindowBounds(context)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.N)
+private object BoundsHelperApi24Impl : BoundsHelper {
+
+    /**
+     * Computes the window bounds for platforms between [Build.VERSION_CODES.N] and
+     * [Build.VERSION_CODES.O_MR1], inclusive.
+     *
+     * NOTE: This method may result in incorrect values under the following conditions:
+     * * If the activity is in multi-window mode the origin of the returned bounds will always be
+     *   anchored at (0, 0).
+     * * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does not
+     *   match the true navigation bar size the returned bounds will not take into account the
+     *   navigation bar.
+     */
+    override fun currentWindowBounds(activity: Activity): Rect {
+        val bounds = Rect()
+        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+        // compatibility with older versions
+        @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+        // [Display#getRectSize] is deprecated but we have this for
+        // compatibility with older versions
+        @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+        if (!isInMultiWindowMode(activity)) {
+            // The activity is not in multi-window mode. Check if the addition of the
+            // navigation bar size to Display#getSize() results in the real display size and
+            // if so return this value. If not, return the result of Display#getSize().
+            val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
+            val navigationBarHeight = getNavigationBarHeight(activity)
+            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
+                bounds.bottom += navigationBarHeight
+            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
+                bounds.right += navigationBarHeight
+            }
+        }
+        return bounds
+    }
+
+    override fun maximumWindowBounds(@UiContext context: Context): Rect {
+        return BoundsHelperApi16Impl.maximumWindowBounds(context)
+    }
+}
+
+private object BoundsHelperApi16Impl : BoundsHelper {
+
+    /**
+     * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN] and
+     * [Build.VERSION_CODES.M], inclusive.
+     *
+     * Given that multi-window mode isn't supported before N we simply return the real display size
+     * which should match the window size of a full-screen app.
+     */
+    override fun currentWindowBounds(activity: Activity): Rect {
+        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+        // compatibility with older versions
+        @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+        val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
+        val bounds = Rect()
+        if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
+            // [Display#getRectSize] is deprecated but we have this for
+            // compatibility with older versions
+            @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+        } else {
+            bounds.right = realDisplaySize.x
+            bounds.bottom = realDisplaySize.y
+        }
+        return bounds
+    }
+
+    override fun maximumWindowBounds(@UiContext context: Context): Rect {
+        val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+        // compatibility with older versions, as we can't reliably get the display associated
+        // with a Context through public APIs either.
+        @Suppress("DEPRECATION") val display = wm.defaultDisplay
+        val displaySize = getRealSizeForDisplay(display)
+        return Rect(0, 0, displaySize.x, displaySize.y)
+    }
+}
+
+/**
+ * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
+ *
+ * Note: This is error-prone and is **not** the recommended way to determine the size of the
+ * overlapping region between the navigation bar and a given window. The best approach is to acquire
+ * the [android.view.WindowInsets].
+ */
+private fun getNavigationBarHeight(context: Context): Int {
+    val resources = context.resources
+    val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
+    return if (resourceId > 0) {
+        resources.getDimensionPixelSize(resourceId)
+    } else 0
+}
+
+private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
+    // [WindowManager#getDefaultDisplay] is deprecated but we have this for
+    // compatibility with older versions
+    @Suppress("DEPRECATION") val defaultDisplay = activity.windowManager.defaultDisplay
+    // [Display#getRectSize] is deprecated but we have this for
+    // compatibility with older versions
+    @Suppress("DEPRECATION") defaultDisplay.getRectSize(bounds)
+}
+
+/**
+ * Returns the [DisplayCutout] for the given display. Note that display cutout returned here is for
+ * the display and the insets provided are in the display coordinate system.
+ *
+ * @return the display cutout for the given display.
+ */
+@SuppressLint("BanUncheckedReflection")
+@RequiresApi(Build.VERSION_CODES.P)
+private fun getCutoutForDisplay(display: Display): DisplayCutout? {
+    var displayCutout: DisplayCutout? = null
+    try {
+        val displayInfoClass = Class.forName("android.view.DisplayInfo")
+        val displayInfoConstructor = displayInfoClass.getConstructor()
+        displayInfoConstructor.isAccessible = true
+        val displayInfo = displayInfoConstructor.newInstance()
+        val getDisplayInfoMethod =
+            display.javaClass.getDeclaredMethod("getDisplayInfo", displayInfo.javaClass)
+        getDisplayInfoMethod.isAccessible = true
+        getDisplayInfoMethod.invoke(display, displayInfo)
+        val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
+        displayCutoutField.isAccessible = true
+        val cutout = displayCutoutField[displayInfo]
+        if (cutout is DisplayCutout) {
+            displayCutout = cutout
+        }
+    } catch (e: Exception) {
+        when (e) {
+            is ClassNotFoundException,
+            is NoSuchMethodException,
+            is NoSuchFieldException,
+            is IllegalAccessException,
+            is InvocationTargetException,
+            is InstantiationException -> {
+                Log.w(TAG, e)
+            }
+            else -> throw e
+        }
+    }
+    return displayCutout
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
index 36fc60b..9207d98 100644
--- a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
+++ b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
@@ -19,14 +19,12 @@
 import android.app.Activity
 import android.content.Context
 import android.content.ContextWrapper
-import android.graphics.Rect
 import android.inputmethodservice.InputMethodService
 import android.os.Build
 import android.view.WindowManager
 import androidx.annotation.RequiresApi
 import androidx.annotation.UiContext
 import androidx.core.view.WindowInsetsCompat
-import androidx.window.layout.WindowMetrics
 
 internal object ContextCompatHelper {
     /**
@@ -59,32 +57,9 @@
     }
 }
 
-@RequiresApi(Build.VERSION_CODES.N)
-internal object ContextCompatHelperApi24 {
-    fun isInMultiWindowMode(activity: Activity): Boolean {
-        return activity.isInMultiWindowMode
-    }
-}
-
 @RequiresApi(Build.VERSION_CODES.R)
 internal object ContextCompatHelperApi30 {
 
-    fun currentWindowMetrics(@UiContext context: Context): WindowMetrics {
-        val wm = context.getSystemService(WindowManager::class.java)
-        val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
-        return WindowMetrics(wm.currentWindowMetrics.bounds, insets)
-    }
-
-    fun currentWindowBounds(@UiContext context: Context): Rect {
-        val wm = context.getSystemService(WindowManager::class.java)
-        return wm.currentWindowMetrics.bounds
-    }
-
-    fun maximumWindowBounds(@UiContext context: Context): Rect {
-        val wm = context.getSystemService(WindowManager::class.java)
-        return wm.maximumWindowMetrics.bounds
-    }
-
     /**
      * Computes the [WindowInsetsCompat] for platforms above [Build.VERSION_CODES.R], inclusive.
      *
diff --git a/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
new file mode 100644
index 0000000..96408ed
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import android.util.DisplayMetrics
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+
+/** Provides compatibility behavior for functionality related to display density. */
+internal interface DensityCompatHelper {
+
+    /** Returns the logical density of the display associated with the [Context]. */
+    fun density(context: Context): Float
+
+    /**
+     * Returns the logical density of the display associated with the [Configuration] or
+     * [AndroidWindowMetrics], depending on the SDK level.
+     */
+    fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float
+
+    companion object {
+        fun getInstance(): DensityCompatHelper {
+            return when {
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
+                    DensityCompatHelperApi34Impl
+                else -> DensityCompatHelperBaseImpl
+            }
+        }
+    }
+}
+
+private object DensityCompatHelperBaseImpl : DensityCompatHelper {
+    override fun density(context: Context): Float {
+        return context.resources.displayMetrics.density
+    }
+
+    override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
+        return configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private object DensityCompatHelperApi34Impl : DensityCompatHelper {
+    override fun density(@UiContext context: Context): Float {
+        return context.getSystemService(WindowManager::class.java).currentWindowMetrics.density
+    }
+
+    override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
+        return windowMetrics.density
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
new file mode 100644
index 0000000..30463dc
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.graphics.Point
+import android.view.Display
+
+internal object DisplayHelper {
+
+    /**
+     * Returns the full (real) size of the display, in pixels, without subtracting any window decor
+     * or applying any compatibility scale factors.
+     *
+     * The size is adjusted based on the current rotation of the display.
+     *
+     * @return a point representing the real display size in pixels.
+     * @see Display.getRealSize
+     */
+    @Suppress("DEPRECATION")
+    fun getRealSizeForDisplay(display: Display): Point {
+        val size = Point()
+        display.getRealSize(size)
+        return size
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
new file mode 100644
index 0000000..313ebf6
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.layout.util
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Rect
+import android.inputmethodservice.InputMethodService
+import android.os.Build
+import android.view.WindowManager
+import android.view.WindowMetrics as AndroidWindowMetrics
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.core.Bounds
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
+import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
+import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
+import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
+
+/** Provides compatibility behavior for functionality related to [WindowMetricsCalculator]. */
+internal interface WindowMetricsCompatHelper {
+
+    /** Translates platform [AndroidWindowMetrics] to jetpack [WindowMetrics]. */
+    @RequiresApi(Build.VERSION_CODES.R)
+    fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics, density: Float): WindowMetrics
+
+    /** Returns the [WindowMetrics] associated with the provided [Context]. */
+    fun currentWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics
+
+    /** Returns the [WindowMetrics] associated with the provided [Activity]. */
+    fun currentWindowMetrics(
+        activity: Activity,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics
+
+    /** Returns the maximum [WindowMetrics] for a given [UiContext]. */
+    fun maximumWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics
+
+    companion object {
+        fun getInstance(): WindowMetricsCompatHelper {
+            return when {
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
+                    WindowMetricsCompatHelperApi34Impl
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> WindowMetricsCompatHelperApi30Impl
+                else -> WindowMetricsCompatHelperBaseImpl
+            }
+        }
+    }
+}
+
+internal object WindowMetricsCompatHelperBaseImpl : WindowMetricsCompatHelper {
+
+    override fun translateWindowMetrics(
+        windowMetrics: AndroidWindowMetrics,
+        density: Float
+    ): WindowMetrics {
+        throw UnsupportedOperationException("translateWindowMetrics not available before API30")
+    }
+
+    override fun currentWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        when (val unwrappedContext = unwrapUiContext(context)) {
+            is Activity -> {
+                return currentWindowMetrics(unwrappedContext, densityCompatHelper)
+            }
+            is InputMethodService -> {
+                val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+                // On older SDK levels, the app and IME could show up on different displays.
+                // However, there isn't a way for us to figure this out from the application
+                // layer. But, this should be good enough for now given the small likelihood of
+                // IMEs showing up on non-primary displays on these SDK levels.
+                @Suppress("DEPRECATION") val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
+
+                // IME occupies the whole display bounds.
+                val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
+                return WindowMetrics(imeBounds, density = densityCompatHelper.density(context))
+            }
+            else -> {
+                throw IllegalArgumentException("$context is not a UiContext")
+            }
+        }
+    }
+
+    override fun currentWindowMetrics(
+        activity: Activity,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        // TODO (b/233899790): compute insets for other platform versions below R
+        return WindowMetrics(
+            Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
+            WindowInsetsCompat.Builder().build(),
+            densityCompatHelper.density(activity)
+        )
+    }
+
+    override fun maximumWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        // TODO (b/233899790): compute insets for other platform versions below Rs
+        return WindowMetrics(
+            Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
+            WindowInsetsCompat.Builder().build(),
+            densityCompatHelper.density(context)
+        )
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+internal object WindowMetricsCompatHelperApi30Impl : WindowMetricsCompatHelper {
+
+    override fun translateWindowMetrics(
+        windowMetrics: AndroidWindowMetrics,
+        density: Float
+    ): WindowMetrics {
+        return WindowMetrics(
+            windowMetrics.bounds,
+            WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
+            density
+        )
+    }
+
+    override fun currentWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        val wm = context.getSystemService(WindowManager::class.java)
+        val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
+        val density = context.resources.displayMetrics.density
+        return WindowMetrics(wm.currentWindowMetrics.bounds, insets, density)
+    }
+
+    override fun currentWindowMetrics(
+        activity: Activity,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        return WindowMetrics(
+            Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
+            currentWindowInsets(activity),
+            densityCompatHelper.density(activity)
+        )
+    }
+
+    override fun maximumWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        return WindowMetrics(
+            Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
+            currentWindowInsets(context),
+            densityCompatHelper.density(context)
+        )
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+internal object WindowMetricsCompatHelperApi34Impl : WindowMetricsCompatHelper {
+
+    override fun translateWindowMetrics(
+        windowMetrics: AndroidWindowMetrics,
+        density: Float
+    ): WindowMetrics {
+        return WindowMetrics(
+            windowMetrics.bounds,
+            WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
+            windowMetrics.density
+        )
+    }
+
+    override fun currentWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        val wm = context.getSystemService(WindowManager::class.java)
+        return WindowMetrics(
+            wm.currentWindowMetrics.bounds,
+            WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets),
+            wm.currentWindowMetrics.density
+        )
+    }
+
+    override fun currentWindowMetrics(
+        activity: Activity,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        return WindowMetricsCompatHelperApi30Impl.currentWindowMetrics(
+            activity,
+            densityCompatHelper
+        )
+    }
+
+    override fun maximumWindowMetrics(
+        @UiContext context: Context,
+        densityCompatHelper: DensityCompatHelper
+    ): WindowMetrics {
+        return WindowMetricsCompatHelperApi30Impl.maximumWindowMetrics(context, densityCompatHelper)
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
index d437ab35..71bfe6a 100644
--- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
+++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
@@ -18,6 +18,7 @@
 
 import android.util.Log
 import java.lang.reflect.Constructor
+import java.lang.reflect.Field
 import java.lang.reflect.Method
 import java.lang.reflect.Modifier
 import kotlin.reflect.KClass
@@ -42,10 +43,10 @@
      * result from the [block]
      */
     @JvmStatic
-    internal fun validateReflection(errorMessage: String? = null, block: () -> Boolean): Boolean {
+    internal fun validateReflection(errorMessage: String, block: () -> Boolean): Boolean {
         return try {
             val result = block()
-            if (!result && errorMessage != null) {
+            if (!result) {
                 Log.e("ReflectionGuard", errorMessage)
             }
             result
@@ -55,6 +56,9 @@
         } catch (noMethod: NoSuchMethodException) {
             Log.e("ReflectionGuard", "NoSuchMethod: ${errorMessage.orEmpty()}")
             false
+        } catch (noField: NoSuchFieldException) {
+            Log.e("ReflectionGuard", "NoSuchField: ${errorMessage.orEmpty()}")
+            false
         }
     }
 
@@ -70,6 +74,12 @@
             return Modifier.isPublic(modifiers)
         }
 
+    /** Checks if a field has public modifier */
+    internal val Field.isPublic: Boolean
+        get() {
+            return Modifier.isPublic(modifiers)
+        }
+
     /** Checks if a method's return value is type of kotlin [clazz] */
     internal fun Method.doesReturn(clazz: KClass<*>): Boolean {
         return doesReturn(clazz.java)
diff --git a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
index dc2bfd8..1a72e56 100644
--- a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
+++ b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
@@ -31,19 +31,29 @@
     /** Constant name for class [androidx.window.extensions.WindowExtensions] used for reflection */
     internal const val WINDOW_EXTENSIONS_CLASS = "$WINDOW_EXTENSIONS_PACKAGE_NAME.WindowExtensions"
 
+    internal const val LAYOUT_PACKAGE = "layout"
+
     /**
      * Constant name for class [androidx.window.extensions.layout.FoldingFeature] used for
      * reflection
      */
     internal const val FOLDING_FEATURE_CLASS =
-        "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.FoldingFeature"
+        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.FoldingFeature"
+
+    /** Constant name for class [androidx.window.extensions.layout.SupportedWindowFeatures] */
+    internal const val SUPPORTED_WINDOW_FEATURES_CLASS =
+        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.SupportedWindowFeatures"
+
+    /** Constant name for class [androidx.window.extensions.layout.DisplayFoldFeature] */
+    internal const val DISPLAY_FOLD_FEATURE_CLASS =
+        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.DisplayFoldFeature"
 
     /**
      * Constant name for class [androidx.window.extensions.layout.WindowLayoutComponent] used for
      * reflection
      */
     internal const val WINDOW_LAYOUT_COMPONENT_CLASS =
-        "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.WindowLayoutComponent"
+        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.WindowLayoutComponent"
 
     /**
      * Constant name for class [androidx.window.extensions.area.WindowAreaComponent] used for
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index 5342612..04a4c09 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -43,7 +43,7 @@
          `ActivityRule`. The suggested usage is to set the tag to be able to differentiate between
          different rules in the callbacks.
          For example, it can be used to compute the right `SplitAttributes` for the given split rule
-         in `SplitAttributes` calculator function. -->
+         in `SplitAttributesCalculator.computeSplitAttributesForParams`. -->
     <attr name="tag" format="string" />
     <!-- An attribute for Activity Embedding rules.
          Background color of Activity Embedding window animation if the animation requires a
@@ -51,6 +51,31 @@
          The default is to use the current theme's windowBackground. Any non-opaque color will be
          treated the same as default. -->
     <attr name="animationBackgroundColor" format="color" />
+    <!-- An attribute for Activity Embedding rules.
+     Defines the open animation transition spec for the Activity Embedding split. -->
+    <attr name="splitOpenAnimation" format="enum">
+        <!-- The default animation specified by the system. -->
+        <enum name="systemDefault" value="0" />
+        <!-- An animation with zero duration. -->
+        <enum name="jumpCut" value="1" />
+    </attr>
+    <!-- An attribute for Activity Embedding rules.
+     Defines the close animation transition spec for the Activity Embedding split. -->
+    <attr name="splitCloseAnimation" format="enum">
+        <!-- The default animation specified by the system. -->
+        <enum name="systemDefault" value="0" />
+        <!-- An animation with zero duration. -->
+        <enum name="jumpCut" value="1" />
+    </attr>
+    <!-- An attribute for Activity Embedding rules.
+         Defines the change (resize or move) animation transition spec for the Activity Embedding
+         split. -->
+    <attr name="splitChangeAnimation" format="enum">
+        <!-- The default animation specified by the system. -->
+        <enum name="systemDefault" value="0" />
+        <!-- An animation with zero duration. -->
+        <enum name="jumpCut" value="1" />
+    </attr>
 
     <!-- An attribute for Activity Embedding rules.
          The smallest value of width of the parent Task bounds when the split should be used, in DP.
@@ -161,6 +186,9 @@
         <attr name="splitLayoutDirection"/>
         <attr name="tag"/>
         <attr name="animationBackgroundColor"/>
+        <attr name="splitOpenAnimation"/>
+        <attr name="splitCloseAnimation"/>
+        <attr name="splitChangeAnimation"/>
     </declare-styleable>
 
     <!-- Attributes that are read when parsing a <SplitPlaceholderRule> tag, which defines the split
@@ -187,6 +215,9 @@
         <attr name="splitLayoutDirection"/>
         <attr name="tag"/>
         <attr name="animationBackgroundColor"/>
+        <attr name="splitOpenAnimation"/>
+        <attr name="splitCloseAnimation"/>
+        <attr name="splitChangeAnimation"/>
     </declare-styleable>
 
     <!-- Attributes that are read when parsing an <ActivityRule> tag, which defines the layout
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
index 6f671e9..6a414bf 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingControllerTest.kt
@@ -18,19 +18,33 @@
 
 import android.app.Activity
 import android.content.Context
-import androidx.window.core.ExperimentalWindowApi
+import android.graphics.Rect
+import androidx.core.util.Consumer
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 /** The unit tests for [ActivityEmbeddingController]. */
+@OptIn(ExperimentalCoroutinesApi::class)
 class ActivityEmbeddingControllerTest {
 
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
     private lateinit var mockEmbeddingBackend: EmbeddingBackend
     private lateinit var mockContext: Context
     private lateinit var mockActivity: Activity
@@ -58,11 +72,73 @@
     }
 
     @Test
-    @OptIn(ExperimentalWindowApi::class)
     fun testGetActivityStack() {
         val activityStack = ActivityStack(listOf(), true)
         whenever(mockEmbeddingBackend.getActivityStack(mockActivity)).thenReturn(activityStack)
 
         assertEquals(activityStack, activityEmbeddingController.getActivityStack(mockActivity))
     }
+
+    @Test
+    fun testFinishActivityStacks() {
+        val activityStacks: Set<ActivityStack> = mock()
+        activityEmbeddingController.finishActivityStacks(activityStacks)
+
+        verify(mockEmbeddingBackend).finishActivityStacks(activityStacks)
+    }
+
+    @Test
+    fun test_invalidateTopVisibleSplitAttributes_delegates() {
+        activityEmbeddingController.invalidateVisibleActivityStacks()
+        verify(mockEmbeddingBackend).invalidateVisibleActivityStacks()
+    }
+
+    @Test
+    fun test_embeddedActivityWindowInfo_delegates() =
+        testScope.runTest {
+            val expectedInfo =
+                EmbeddedActivityWindowInfo(
+                    isEmbedded = true,
+                    parentHostBounds = Rect(0, 0, 1000, 2000),
+                    boundsInParentHost = Rect(0, 0, 500, 2000)
+                )
+            doAnswer { invocationOnMock ->
+                    @Suppress("UNCHECKED_CAST")
+                    val callback =
+                        invocationOnMock.arguments.last() as Consumer<EmbeddedActivityWindowInfo>
+                    callback.accept(expectedInfo)
+                }
+                .whenever(mockEmbeddingBackend)
+                .addEmbeddedActivityWindowInfoCallbackForActivity(any(), any())
+
+            val actualInfo =
+                activityEmbeddingController
+                    .embeddedActivityWindowInfo(mockActivity)
+                    .take(1)
+                    .toList()
+                    .first()
+
+            assertEquals(expectedInfo, actualInfo)
+            verify(mockEmbeddingBackend)
+                .addEmbeddedActivityWindowInfoCallbackForActivity(eq(mockActivity), any())
+            verify(mockEmbeddingBackend).removeEmbeddedActivityWindowInfoCallbackForActivity(any())
+        }
+
+    @Test
+    fun testGetInstance() {
+        EmbeddingBackend.overrideDecorator(
+            object : EmbeddingBackendDecorator {
+                override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
+                    mockEmbeddingBackend
+            }
+        )
+        val controller = ActivityEmbeddingController.getInstance(mockActivity)
+        val activityStacks: Set<ActivityStack> = mock()
+
+        controller.finishActivityStacks(activityStacks)
+
+        verify(mockEmbeddingBackend).finishActivityStacks(activityStacks)
+
+        EmbeddingBackend.reset()
+    }
 }
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
new file mode 100644
index 0000000..53fca91
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityEmbeddingOptionsTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * The unit tests for activity embedding extension functions to [Bundle]
+ *
+ * @see Bundle.setLaunchingActivityStack
+ * @see Bundle.setOverlayCreateParams
+ */
+class ActivityEmbeddingOptionsTest {
+
+    @Mock private lateinit var mockEmbeddingBackend: EmbeddingBackend
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockActivity: Activity
+    @Mock private lateinit var mockOptions: Bundle
+
+    private lateinit var annotationClosable: AutoCloseable
+
+    private lateinit var mockActivityStack: ActivityStack
+
+    @Before
+    fun setUp() {
+        annotationClosable = MockitoAnnotations.openMocks(this)
+
+        mockActivityStack =
+            ActivityStack(listOf(), true, ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN)
+        whenever(mockActivity.applicationContext).doReturn(mockContext)
+
+        EmbeddingBackend.overrideDecorator(
+            object : EmbeddingBackendDecorator {
+                override fun decorate(embeddingBackend: EmbeddingBackend): EmbeddingBackend =
+                    mockEmbeddingBackend
+            }
+        )
+    }
+
+    @After
+    fun tearDown() {
+        EmbeddingBackend.reset()
+        annotationClosable.close()
+    }
+
+    @Test
+    fun testSetLaunchingActivityStack() {
+        mockOptions.setLaunchingActivityStack(mockActivity, mockActivityStack)
+
+        verify(mockEmbeddingBackend).setLaunchingActivityStack(mockOptions, mockActivityStack)
+    }
+
+    @Test
+    fun testSetOverlayCreateParams() {
+        val overlayCreateParams = OverlayCreateParams(overlayAttributes = OverlayAttributes())
+        mockOptions.setOverlayCreateParams(mockActivity, overlayCreateParams)
+
+        verify(mockEmbeddingBackend).setOverlayCreateParams(mockOptions, overlayCreateParams)
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
index 7c50304..203e1ff 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityStackTest.kt
@@ -17,8 +17,11 @@
 package androidx.window.embedding
 
 import android.app.Activity
+import android.os.Binder
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
 import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.mockito.kotlin.mock
@@ -37,11 +40,18 @@
     @Test
     fun testEqualsImpliesHashCode() {
         val activity = mock<Activity>()
-        val first = ActivityStack(listOf(activity), isEmpty = false)
-        val second = ActivityStack(listOf(activity), isEmpty = false)
+        val token = ActivityStackToken.createFromBinder(Binder())
+        val first = ActivityStack(listOf(activity), isEmpty = false, token)
+        val second = ActivityStack(listOf(activity), isEmpty = false, token)
 
         assertEquals(first, second)
         assertEquals(first.hashCode(), second.hashCode())
+
+        val anotherToken = ActivityStackToken.createFromBinder(Binder())
+        val third = ActivityStack(emptyList(), isEmpty = true, anotherToken)
+
+        assertNotEquals(first, third)
+        assertNotEquals(first.hashCode(), third.hashCode())
     }
 
     @Test
@@ -59,10 +69,12 @@
     fun testToString() {
         val activitiesInProcess = mock<List<Activity>>()
         val isEmpty = false
+        val token = ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
 
-        val stackString = ActivityStack(activitiesInProcess, isEmpty).toString()
+        val stackString = ActivityStack(activitiesInProcess, isEmpty, token).toString()
 
         assertTrue(stackString.contains(activitiesInProcess.toString()))
         assertTrue(stackString.contains(isEmpty.toString()))
+        assertTrue(stackString.contains(token.toString()))
     }
 }
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt
new file mode 100644
index 0000000..1b90cb5
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityWindowInfoCallbackControllerTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.app.Activity
+import androidx.core.util.Consumer as JetpackConsumer
+import androidx.window.WindowSdkExtensionsRule
+import androidx.window.extensions.core.util.function.Consumer
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Verifies [ActivityWindowInfoCallbackController] */
+@Suppress("GuardedBy")
+class ActivityWindowInfoCallbackControllerTest {
+
+    @get:Rule val testRule = WindowSdkExtensionsRule()
+
+    @Mock private lateinit var embeddingExtension: ActivityEmbeddingComponent
+    @Mock private lateinit var callback1: JetpackConsumer<EmbeddedActivityWindowInfo>
+    @Mock private lateinit var callback2: JetpackConsumer<EmbeddedActivityWindowInfo>
+    @Mock private lateinit var activity1: Activity
+    @Mock private lateinit var activity2: Activity
+
+    @Captor
+    private lateinit var callbackCaptor: ArgumentCaptor<Consumer<ExtensionsActivityWindowInfo>>
+
+    private lateinit var controller: ActivityWindowInfoCallbackController
+    private lateinit var mockAnnotations: AutoCloseable
+
+    @Before
+    fun setUp() {
+        testRule.overrideExtensionVersion(6)
+        mockAnnotations = MockitoAnnotations.openMocks(this)
+
+        controller = ActivityWindowInfoCallbackController(embeddingExtension)
+        // ArrayMap is not available in JVM test
+        controller.callbacks = HashMap()
+        controller = spy(controller)
+    }
+
+    @After
+    fun tearDown() {
+        mockAnnotations.close()
+    }
+
+    @Test
+    fun testAddCallback() {
+        // Register the extensions callback
+        controller.addCallback(activity1, callback1)
+
+        verify(embeddingExtension).setEmbeddedActivityWindowInfoCallback(any(), any())
+
+        // Do not register for the additional callback
+        clearInvocations(embeddingExtension)
+        controller.addCallback(activity1, callback2)
+
+        verify(embeddingExtension, never()).setEmbeddedActivityWindowInfoCallback(any(), any())
+    }
+
+    @Test
+    fun testRemoveCallback() {
+        // Unregister after the last Jetpack callback is removed.
+        controller.addCallback(activity1, callback1)
+        controller.addCallback(activity1, callback2)
+
+        controller.removeCallback(callback1)
+
+        verify(embeddingExtension, never()).clearEmbeddedActivityWindowInfoCallback()
+
+        controller.removeCallback(callback2)
+
+        verify(embeddingExtension).clearEmbeddedActivityWindowInfoCallback()
+    }
+
+    @Test
+    fun testActivityWindowInfoChanged() {
+        controller.addCallback(activity1, callback1)
+        controller.addCallback(activity1, callback2)
+
+        verify(embeddingExtension)
+            .setEmbeddedActivityWindowInfoCallback(any(), callbackCaptor.capture())
+        val extensionsCallback = callbackCaptor.value
+
+        val extensionsInfo: ExtensionsActivityWindowInfo = mock()
+        val expectedInfo =
+            EmbeddedActivityWindowInfo(
+                isEmbedded = true,
+                parentHostBounds = mock(),
+                boundsInParentHost = mock()
+            )
+        doReturn(expectedInfo).whenever(controller).translate(extensionsInfo)
+
+        // No callback because the activity doesn't match.
+        doReturn(activity2).whenever(extensionsInfo).activity
+        extensionsCallback.accept(extensionsInfo)
+
+        verify(callback1, never()).accept(any())
+        verify(callback2, never()).accept(any())
+
+        // Should receive the correct translated info.
+        doReturn(activity1).whenever(extensionsInfo).activity
+        extensionsCallback.accept(extensionsInfo)
+
+        verify(callback1).accept(expectedInfo)
+        verify(callback2).accept(expectedInfo)
+
+        // Do not send unchanged info.
+        clearInvocations(callback1)
+        clearInvocations(callback2)
+        extensionsCallback.accept(extensionsInfo)
+
+        verify(callback1, never()).accept(any())
+        verify(callback2, never()).accept(any())
+
+        // Only the remaining callback can receive the info.
+        controller.removeCallback(callback1)
+        val expectedInfo2 =
+            EmbeddedActivityWindowInfo(
+                isEmbedded = false,
+                parentHostBounds = mock(),
+                boundsInParentHost = mock()
+            )
+        doReturn(expectedInfo2).whenever(controller).translate(extensionsInfo)
+        extensionsCallback.accept(extensionsInfo)
+
+        verify(callback1, never()).accept(any())
+        verify(callback2).accept(any())
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt
new file mode 100644
index 0000000..1676367
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/DividerAttributesTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Color
+import androidx.window.embedding.DividerAttributes.DragRange.Companion.DRAG_RANGE_SYSTEM_DEFAULT
+import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+/** Test class to verify [DividerAttributes] */
+class DividerAttributesTest {
+
+    @Test
+    fun testDividerAttributesEquals() {
+        val attrs1 = FixedDividerAttributes.Builder().build()
+        val attrs2 = DraggableDividerAttributes.Builder().build()
+        val attrs3 =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(20)
+                .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+                .build()
+        val attrs4 =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(DividerAttributes.WIDTH_SYSTEM_DEFAULT)
+                .setDragRange(DRAG_RANGE_SYSTEM_DEFAULT)
+                .build()
+        val attrs5 =
+            DraggableDividerAttributes.Builder(attrs4).setDraggingToFullscreenAllowed(true).build()
+
+        assertNotEquals(attrs1, attrs2)
+        assertNotEquals(attrs1.hashCode(), attrs2.hashCode())
+
+        assertNotEquals(attrs2, attrs3)
+        assertNotEquals(attrs2.hashCode(), attrs3.hashCode())
+
+        assertNotEquals(attrs3, attrs4)
+        assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
+
+        assertNotEquals(attrs4, attrs5)
+        assertNotEquals(attrs4.hashCode(), attrs5.hashCode())
+
+        // attrs2 and attrs4 must be equal because attrs4 uses default values.
+        assertEquals(attrs2, attrs4)
+        assertEquals(attrs2.hashCode(), attrs4.hashCode())
+    }
+
+    @Test
+    fun testBuilder() {
+        val attrs =
+            DraggableDividerAttributes.Builder()
+                .setWidthDp(20)
+                .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
+                .setColor(Color.GRAY)
+                .setDraggingToFullscreenAllowed(true)
+                .build()
+
+        assertEquals(20, attrs.widthDp)
+        assertEquals(SplitRatioDragRange(0.3f, 0.7f), attrs.dragRange)
+        assertEquals(Color.GRAY, attrs.color)
+        assertEquals(true, attrs.isDraggingToFullscreenAllowed)
+    }
+
+    @Test
+    fun testBuilder_defaultValues() {
+        val attrs = DraggableDividerAttributes.Builder().build()
+
+        assertEquals(DividerAttributes.WIDTH_SYSTEM_DEFAULT, attrs.widthDp)
+        assertEquals(DRAG_RANGE_SYSTEM_DEFAULT, attrs.dragRange)
+        assertEquals(Color.BLACK, attrs.color)
+        assertEquals(false, attrs.isDraggingToFullscreenAllowed)
+    }
+
+    @Test
+    fun testSplitRatioDragRange_validation() {
+        // Valid range
+        SplitRatioDragRange(minRatio = 0.2f, maxRatio = 0.8f)
+
+        // Invalid minRatio and maxRatio
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitRatioDragRange(minRatio = 0.0f, maxRatio = 1.0f)
+        }
+
+        // Invalid minRatio
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitRatioDragRange(minRatio = -1.0f, maxRatio = 0.8f)
+        }
+
+        // Invalid maxRatio
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitRatioDragRange(minRatio = 0.2f, maxRatio = 1.2f)
+        }
+
+        // minRatio should not be less than maxRatio
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitRatioDragRange(minRatio = 0.6f, maxRatio = 0.4f)
+        }
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt b/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt
new file mode 100644
index 0000000..d5df40d
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/EmbeddingBoundsTests.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.graphics.Rect
+import androidx.window.core.Bounds
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Unit tests for [EmbeddingBounds] */
+class EmbeddingBoundsTests {
+
+    private val taskBounds = Bounds(0, 0, 10, 10)
+
+    private val layoutInfoWithoutHinge = WindowLayoutInfo(emptyList())
+
+    private val layoutInfoWithTwoHinges =
+        WindowLayoutInfo(
+            listOf(
+                TestFoldingFeature(Bounds(left = 4, top = 0, right = 6, bottom = 10)),
+                TestFoldingFeature(Bounds(left = 0, top = 4, right = 10, bottom = 6)),
+            )
+        )
+
+    private val layoutInfoWithVerticalHinge =
+        WindowLayoutInfo(
+            listOf(TestFoldingFeature(Bounds(left = 4, top = 0, right = 6, bottom = 10)))
+        )
+
+    private val layoutInfoWithHorizontalHinge =
+        WindowLayoutInfo(
+            listOf(TestFoldingFeature(Bounds(left = 0, top = 4, right = 10, bottom = 6)))
+        )
+
+    @Test
+    fun testTranslateBOUNDS_EXPANDED_returnEmptyBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                        EmbeddingBounds.BOUNDS_EXPANDED,
+                        taskBounds,
+                        layoutInfoWithVerticalHinge
+                    )
+                    .isZero
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun testTranslateBoundsMatchParentTask_returnEmptyBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                        EmbeddingBounds(
+                            EmbeddingBounds.Alignment.ALIGN_TOP,
+                            width = EmbeddingBounds.Dimension.pixel(taskBounds.width),
+                            height = EmbeddingBounds.Dimension.pixel(taskBounds.height),
+                        ),
+                        taskBounds,
+                        layoutInfoWithVerticalHinge
+                    )
+                    .isZero
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun testTranslateBoundsLargerThanParentTask_returnEmptyBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                        EmbeddingBounds(
+                            EmbeddingBounds.Alignment.ALIGN_TOP,
+                            width = EmbeddingBounds.Dimension.pixel(taskBounds.width + 1),
+                            height = EmbeddingBounds.Dimension.pixel(taskBounds.height + 1),
+                        ),
+                        taskBounds,
+                        layoutInfoWithVerticalHinge
+                    )
+                    .isZero
+            )
+            .isTrue()
+    }
+
+    @Test
+    fun testTranslateBOUNDS_HINGE_LEFT() {
+        val fallbackBoundsHingeLeft = Bounds(left = 0, top = 0, right = 5, bottom = 10)
+
+        assertWithMessage("Must fallback to the left half on device without hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_LEFT,
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeLeft)
+
+        assertWithMessage("Must fallback to the left half on device with multiple hinges")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_LEFT,
+                    taskBounds,
+                    layoutInfoWithTwoHinges,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeLeft)
+
+        assertWithMessage("Must fallback to the left half on device with a horizontal hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_LEFT,
+                    taskBounds,
+                    layoutInfoWithHorizontalHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeLeft)
+
+        assertWithMessage("Must report bounds on the left of hinge on device with a vertical hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_LEFT,
+                    taskBounds,
+                    layoutInfoWithVerticalHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 0, top = 0, right = 4, bottom = 10))
+    }
+
+    @Test
+    fun testTranslateBOUNDS_HINGE_TOP() {
+        val fallbackBoundsHingeTop = Bounds(left = 0, top = 0, right = 10, bottom = 5)
+
+        assertWithMessage("Must fallback to the top half on device without hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_TOP,
+                    taskBounds,
+                    layoutInfoWithoutHinge
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeTop)
+
+        assertWithMessage("Must fallback to the top half on device with multiple hinges")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_TOP,
+                    taskBounds,
+                    layoutInfoWithTwoHinges,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeTop)
+
+        assertWithMessage("Must fallback to the top half on device with a vertical hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_TOP,
+                    taskBounds,
+                    layoutInfoWithVerticalHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeTop)
+
+        assertWithMessage(
+                "Must report bounds on the top of hinge on device with a horizontal hinge"
+            )
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_TOP,
+                    taskBounds,
+                    layoutInfoWithHorizontalHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 0, top = 0, right = 10, bottom = 4))
+    }
+
+    @Test
+    fun testTranslateBOUNDS_HINGE_RIGHT() {
+        val fallbackBoundsHingeRight = Bounds(left = 5, top = 0, right = 10, bottom = 10)
+
+        assertWithMessage("Must fallback to the right half on device without hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeRight)
+
+        assertWithMessage("Must fallback to the right half on device with multiple hinges")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+                    taskBounds,
+                    layoutInfoWithTwoHinges,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeRight)
+
+        assertWithMessage("Must fallback to the right half on device with a horizontal hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+                    taskBounds,
+                    layoutInfoWithHorizontalHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeRight)
+
+        assertWithMessage(
+                "Must report bounds on the right of hinge on device with a vertical hinge"
+            )
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_RIGHT,
+                    taskBounds,
+                    layoutInfoWithVerticalHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 6, top = 0, right = 10, bottom = 10))
+    }
+
+    @Test
+    fun testTranslateBOUNDS_HINGE_BOTTOM() {
+        val fallbackBoundsHingeBottom = Bounds(left = 0, top = 5, right = 10, bottom = 10)
+
+        assertWithMessage("Must fallback to the bottom half on device without hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeBottom)
+
+        assertWithMessage("Must fallback to the bottom half on device with multiple hinges")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+                    taskBounds,
+                    layoutInfoWithTwoHinges,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeBottom)
+
+        assertWithMessage("Must fallback to the bottom half on device with a vertical hinge")
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+                    taskBounds,
+                    layoutInfoWithVerticalHinge,
+                )
+            )
+            .isEqualTo(fallbackBoundsHingeBottom)
+
+        assertWithMessage(
+                "Must report bounds on the bottom of hinge on device with a horizontal hinge"
+            )
+            .that(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds.BOUNDS_HINGE_BOTTOM,
+                    taskBounds,
+                    layoutInfoWithHorizontalHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 0, top = 6, right = 10, bottom = 10))
+    }
+
+    @Test
+    fun testTranslateShrunkLeftBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds(
+                        EmbeddingBounds.Alignment.ALIGN_LEFT,
+                        EmbeddingBounds.Dimension.ratio(0.7f),
+                        EmbeddingBounds.Dimension.pixel(8),
+                    ),
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 0, top = 1, right = 7, bottom = 9))
+    }
+
+    @Test
+    fun testTranslateShrunkTopBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds(
+                        EmbeddingBounds.Alignment.ALIGN_TOP,
+                        EmbeddingBounds.Dimension.pixel(8),
+                        EmbeddingBounds.Dimension.ratio(0.5f),
+                    ),
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 1, top = 0, right = 9, bottom = 5))
+    }
+
+    @Test
+    fun testTranslateShrunkBottomBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds(
+                        EmbeddingBounds.Alignment.ALIGN_BOTTOM,
+                        EmbeddingBounds.Dimension.DIMENSION_HINGE,
+                        EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
+                    ),
+                    taskBounds,
+                    layoutInfoWithoutHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 2, top = 0, right = 7, bottom = 10))
+    }
+
+    @Test
+    fun testTranslateShrunkRightBounds() {
+        assertThat(
+                EmbeddingBounds.translateEmbeddingBounds(
+                    EmbeddingBounds(
+                        EmbeddingBounds.Alignment.ALIGN_RIGHT,
+                        EmbeddingBounds.Dimension.DIMENSION_HINGE,
+                        EmbeddingBounds.Dimension.pixel(4),
+                    ),
+                    taskBounds,
+                    layoutInfoWithVerticalHinge,
+                )
+            )
+            .isEqualTo(Bounds(left = 6, top = 3, right = 10, bottom = 7))
+    }
+
+    private class TestFoldingFeature(val rawBounds: Bounds) : FoldingFeature {
+        override val bounds: Rect
+            get() =
+                mock<Rect>().apply {
+                    left = rawBounds.left
+                    top = rawBounds.top
+                    right = rawBounds.right
+                    bottom = rawBounds.bottom
+                    doReturn(rawBounds.width).whenever(this).width()
+                    doReturn(rawBounds.height).whenever(this).height()
+                }
+
+        override val isSeparating: Boolean
+            get() = true
+
+        override val occlusionType: FoldingFeature.OcclusionType
+            get() = FoldingFeature.OcclusionType.FULL
+
+        override val orientation: FoldingFeature.Orientation
+            get() =
+                if (rawBounds.width > rawBounds.height) {
+                    FoldingFeature.Orientation.HORIZONTAL
+                } else {
+                    FoldingFeature.Orientation.VERTICAL
+                }
+
+        override val state: FoldingFeature.State
+            get() = FoldingFeature.State.FLAT
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt b/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
index 03df207..4f88743 100644
--- a/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/EmbeddingCompatTest.kt
@@ -17,12 +17,14 @@
 package androidx.window.embedding
 
 import android.app.Activity
+import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
-import androidx.window.core.ExtensionsUtil
 import androidx.window.core.PredicateAdapter
 import androidx.window.extensions.core.util.function.Consumer
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
 import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import java.util.concurrent.Executor
 import java.util.function.Consumer as JavaConsumer
 import org.junit.Test
 import org.mockito.kotlin.any
@@ -32,9 +34,16 @@
 class EmbeddingCompatTest {
 
     private val component = mock<ActivityEmbeddingComponent>()
-    private val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
+    private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
     private val embeddingCompat =
-        EmbeddingCompat(component, EMBEDDING_ADAPTER, CONSUMER_ADAPTER, mock())
+        EmbeddingCompat(
+            component,
+            EMBEDDING_ADAPTER,
+            CONSUMER_ADAPTER,
+            mock(),
+            mock(),
+            mock(),
+        )
 
     @Suppress("Deprecation")
     @Test
@@ -42,13 +51,22 @@
         val callback =
             object : EmbeddingInterfaceCompat.EmbeddingCallbackInterface {
                 override fun onSplitInfoChanged(splitInfo: List<SplitInfo>) {}
+
+                override fun onActivityStackChanged(activityStacks: List<ActivityStack>) {}
             }
         embeddingCompat.setEmbeddingCallback(callback)
 
-        if (vendorApiLevel < 2) {
-            verify(component).setSplitInfoCallback(any<JavaConsumer<List<OEMSplitInfo>>>())
-        } else {
-            verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+        when (extensionVersion) {
+            1 -> verify(component).setSplitInfoCallback(any<JavaConsumer<List<OEMSplitInfo>>>())
+            in 2..4 -> verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+            5 -> {
+                verify(component).setSplitInfoCallback(any<Consumer<List<OEMSplitInfo>>>())
+                verify(component)
+                    .registerActivityStackCallback(
+                        any<Executor>(),
+                        any<Consumer<List<OEMActivityStack>>>()
+                    )
+            }
         }
     }
 
diff --git a/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt b/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt
new file mode 100644
index 0000000..aa2bb20
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/OverlayControllerImplTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.annotation.SuppressLint
+import android.content.res.Configuration
+import android.graphics.Rect
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.WindowSdkExtensionsRule
+import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.mock
+
+/** Verifies [OverlayControllerImpl] */
+@Suppress("GuardedBy")
+class OverlayControllerImplTest {
+
+    @get:Rule val testRule = WindowSdkExtensionsRule()
+
+    private lateinit var overlayController: TestableOverlayControllerImpl
+
+    @Before
+    fun setUp() {
+        testRule.overrideExtensionVersion(OVERLAY_FEATURE_VERSION)
+
+        overlayController = TestableOverlayControllerImpl()
+    }
+
+    /** Verifies the behavior of [OverlayControllerImpl.calculateOverlayAttributes] */
+    @SuppressLint("NewApi")
+    @Test
+    fun testCalculateOverlayAttributes() {
+        assertThat(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+            .isEqualTo(DEFAULT_OVERLAY_ATTRS)
+
+        overlayController.overlayAttributesCalculator = { _ -> CALCULATED_OVERLAY_ATTRS }
+
+        assertWithMessage("Calculated overlay attrs must be reported if calculator exists.")
+            .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+            .isEqualTo(CALCULATED_OVERLAY_ATTRS)
+
+        overlayController.updateOverlayAttributes(TAG_TEST, UPDATED_OVERLAY_ATTRS)
+
+        assertWithMessage("Calculated overlay attrs must be reported if calculator exists.")
+            .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+            .isEqualTo(CALCULATED_OVERLAY_ATTRS)
+
+        overlayController.overlayAttributesCalculator = null
+
+        assertWithMessage("#updateOverlayAttributes should also update the current overlay attrs.")
+            .that(overlayController.calculateOverlayAttributes(DEFAULT_OVERLAY_ATTRS))
+            .isEqualTo(UPDATED_OVERLAY_ATTRS)
+    }
+
+    private fun OverlayControllerImpl.calculateOverlayAttributes(
+        initialOverlayAttrs: OverlayAttributes?
+    ): OverlayAttributes =
+        calculateOverlayAttributes(
+            TAG_TEST,
+            initialOverlayAttrs,
+            WindowMetrics(Rect(), WindowInsetsCompat.CONSUMED, density = 1f),
+            Configuration(),
+            WindowLayoutInfo(emptyList())
+        )
+
+    companion object {
+        private const val TAG_TEST = "test"
+
+        private val DEFAULT_OVERLAY_ATTRS = OverlayAttributes.Builder().build()
+
+        private val CALCULATED_OVERLAY_ATTRS =
+            OverlayAttributes.Builder().setBounds(EmbeddingBounds.BOUNDS_HINGE_RIGHT).build()
+
+        private val UPDATED_OVERLAY_ATTRS =
+            OverlayAttributes.Builder().setBounds(EmbeddingBounds.BOUNDS_HINGE_BOTTOM).build()
+    }
+
+    private class TestableOverlayControllerImpl(
+        mockExtension: ActivityEmbeddingComponent = mock<ActivityEmbeddingComponent>(),
+    ) :
+        OverlayControllerImpl(
+            mockExtension,
+            EmbeddingAdapter(PredicateAdapter(ClassLoader.getSystemClassLoader()))
+        ) {
+        val overlayTagToAttributesMap = HashMap<String, OverlayAttributes>()
+
+        override fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =
+            overlayTagToAttributesMap[overlayTag]
+
+        override fun updateOverlayAttributes(
+            overlayTag: String,
+            overlayAttributes: OverlayAttributes
+        ) {
+            overlayTagToAttributesMap[overlayTag] = overlayAttributes
+        }
+    }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt
new file mode 100644
index 0000000..c20b535
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/OverlayControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.embedding
+
+import android.os.Bundle
+import androidx.core.util.Consumer
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class OverlayControllerTest {
+    private val mockBackend = mock<EmbeddingBackend>()
+    private val overlayController = OverlayController(mockBackend)
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    @Test
+    fun testSetOverlayCreateParams() {
+        val options = Bundle()
+        val params = OverlayCreateParams()
+        overlayController.setOverlayCreateParams(options, params)
+
+        verify(mockBackend).setOverlayCreateParams(options, params)
+    }
+
+    @Test
+    fun test_overlayAttributesCalculator_delegates() {
+        val calculator = { _: OverlayAttributesCalculatorParams -> OverlayAttributes() }
+
+        overlayController.setOverlayAttributesCalculator(calculator)
+
+        verify(mockBackend).setOverlayAttributesCalculator(calculator)
+
+        overlayController.clearOverlayAttributesCalculator()
+
+        verify(mockBackend).clearOverlayAttributesCalculator()
+    }
+
+    @Test
+    fun test_updateOverlayAttributes_delegates() {
+        val tag = "test"
+        val overlayAttributes = OverlayAttributes()
+
+        overlayController.updateOverlayAttributes(tag, overlayAttributes)
+
+        verify(mockBackend).updateOverlayAttributes(tag, overlayAttributes)
+    }
+
+    @Test
+    fun test_overlayInfoComesFromBackend() =
+        testScope.runTest {
+            val tag = "test"
+            val expected =
+                OverlayInfo(
+                    overlayTag = tag,
+                    currentOverlayAttributes = OverlayAttributes(),
+                    activityStack =
+                        ActivityStack(
+                            emptyList(),
+                            true,
+                            ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
+                        )
+                )
+            doAnswer { invocationOnMock ->
+                    @Suppress("UNCHECKED_CAST")
+                    val listener = invocationOnMock.arguments.last() as Consumer<OverlayInfo>
+                    listener.accept(expected)
+                }
+                .whenever(mockBackend)
+                .addOverlayInfoCallback(any(), any(), any())
+
+            val actual = overlayController.overlayInfo(tag).take(1).toList().first()
+
+            Assert.assertEquals(expected, actual)
+            verify(mockBackend).addOverlayInfoCallback(eq(tag), any(), any())
+            verify(mockBackend).removeOverlayInfoCallback(any())
+        }
+}
diff --git a/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
index 3cd660b..8c3d4de 100644
--- a/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
+++ b/window/window/src/test/java/androidx/window/embedding/RequiresWindowSdkExtensionTests.kt
@@ -16,24 +16,27 @@
 
 package androidx.window.embedding
 
-import android.app.ActivityOptions
 import android.content.Context
-import android.os.Binder
-import android.os.Build
+import android.os.Bundle
 import android.os.IBinder
-import androidx.annotation.RequiresApi
 import androidx.window.RequiresWindowSdkExtension
 import androidx.window.WindowSdkExtensions
 import androidx.window.WindowSdkExtensionsRule
 import androidx.window.core.ConsumerAdapter
 import androidx.window.core.PredicateAdapter
-import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
 import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
+import androidx.window.embedding.OverlayController.Companion.OVERLAY_FEATURE_VERSION
 import androidx.window.extensions.core.util.function.Function
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
+import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties
+import androidx.window.extensions.embedding.ActivityStack.Token as ActivityStackToken
 import androidx.window.extensions.embedding.SplitAttributes as OemSplitAttributes
 import androidx.window.extensions.embedding.SplitAttributesCalculatorParams as OemSplitAttributesCalculatorParams
+import androidx.window.extensions.embedding.SplitInfo.Token as SplitInfoToken
+import java.util.concurrent.Executor
 import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
@@ -41,8 +44,11 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
-import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -53,7 +59,7 @@
  *   successfully
  * - Otherwise, [UnsupportedOperationException] must be thrown.
  */
-@RequiresApi(Build.VERSION_CODES.M) // To call ActivityOptions.makeBasic()
+@Suppress("Deprecation")
 class RequiresWindowSdkExtensionTests {
 
     @get:Rule val testRule = WindowSdkExtensionsRule()
@@ -61,26 +67,18 @@
     @Mock private lateinit var embeddingExtension: ActivityEmbeddingComponent
     @Mock private lateinit var classLoader: ClassLoader
     @Mock private lateinit var applicationContext: Context
-    @Mock private lateinit var activityOptions: ActivityOptions
+    @Mock private lateinit var options: Bundle
 
     private lateinit var mockAnnotations: AutoCloseable
     private lateinit var embeddingCompat: EmbeddingCompat
+    private lateinit var activityStack: ActivityStack
 
-    @Suppress("DEPRECATION")
+    private val activityStackToken = ActivityStackToken.INVALID_ACTIVITY_STACK_TOKEN
+
     @Before
     fun setUp() {
         mockAnnotations = MockitoAnnotations.openMocks(this)
-        embeddingCompat =
-            EmbeddingCompat(
-                embeddingExtension,
-                EmbeddingAdapter(PredicateAdapter(classLoader)),
-                ConsumerAdapter(classLoader),
-                applicationContext
-            )
-
-        doReturn(activityOptions)
-            .whenever(embeddingExtension)
-            .setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+        activityStack = ActivityStack(emptyList(), isEmpty = true, activityStackToken)
     }
 
     @After
@@ -88,10 +86,10 @@
         mockAnnotations.close()
     }
 
-    @Suppress("DEPRECATION")
     @Test
-    fun testVendorApiLevel1() {
+    fun testWindowExtensionsVersion1() {
         testRule.overrideExtensionVersion(1)
+        createTestEmbeddingCompat()
 
         assertThrows(UnsupportedOperationException::class.java) {
             embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
@@ -104,26 +102,34 @@
         verify(embeddingExtension, never()).clearSplitAttributesCalculator()
 
         assertThrows(UnsupportedOperationException::class.java) {
-            embeddingCompat.setLaunchingActivityStack(activityOptions, Binder())
+            embeddingCompat.setLaunchingActivityStack(options, activityStack)
         }
-        verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+        verify(options, never()).putBinder(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never())
+            .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
 
         assertThrows(UnsupportedOperationException::class.java) {
             embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
         }
-        verify(embeddingExtension, never())
-            .updateSplitAttributes(any<IBinder>(), any<OemSplitAttributes>())
+        verify(embeddingExtension, never()).updateSplitAttributes(any<IBinder>(), any())
 
         assertThrows(UnsupportedOperationException::class.java) {
-            embeddingCompat.invalidateTopVisibleSplitAttributes()
+            embeddingCompat.invalidateVisibleActivityStacks()
         }
         verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
     }
 
-    @Suppress("DEPRECATION")
     @Test
-    fun testVendorApiLevel2() {
+    fun testWindowExtensionsVersion2() {
         testRule.overrideExtensionVersion(2)
+        createTestEmbeddingCompat()
 
         embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
         verify(embeddingExtension)
@@ -135,26 +141,42 @@
         verify(embeddingExtension).clearSplitAttributesCalculator()
 
         assertThrows(UnsupportedOperationException::class.java) {
-            embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+            embeddingCompat.setLaunchingActivityStack(options, activityStack)
         }
-        verify(embeddingExtension, never()).setLaunchingActivityStack(any(), any())
+        verify(options, never()).putBundle(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never())
+            .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
 
         assertThrows(UnsupportedOperationException::class.java) {
             embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
         }
-        verify(embeddingExtension, never())
-            .updateSplitAttributes(any<IBinder>(), any<OemSplitAttributes>())
+        verify(embeddingExtension, never()).updateSplitAttributes(any<IBinder>(), any())
 
         assertThrows(UnsupportedOperationException::class.java) {
-            embeddingCompat.invalidateTopVisibleSplitAttributes()
+            embeddingCompat.invalidateVisibleActivityStacks()
         }
         verify(embeddingExtension, never()).invalidateTopVisibleSplitAttributes()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
     }
 
-    @Suppress("DEPRECATION")
     @Test
-    fun testVendorApiLevel3() {
+    fun testWindowExtensionsVersion3() {
         testRule.overrideExtensionVersion(3)
+        createTestEmbeddingCompat()
+
+        val splitInfo =
+            SplitInfo(
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                SplitAttributes.Builder().build(),
+                binder = INVALID_SPLIT_INFO_TOKEN,
+            )
 
         embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
         verify(embeddingExtension)
@@ -165,17 +187,228 @@
         embeddingCompat.clearSplitAttributesCalculator()
         verify(embeddingExtension).clearSplitAttributesCalculator()
 
-        embeddingCompat.setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.setLaunchingActivityStack(options, activityStack)
+        }
+        verify(options, never()).putBundle(any(), any())
 
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never())
+            .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
+
+        embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
         verify(embeddingExtension)
-            .setLaunchingActivityStack(activityOptions, INVALID_ACTIVITY_STACK_TOKEN)
+            .updateSplitAttributes(splitInfo.getBinder(), OemSplitAttributes.Builder().build())
 
-        embeddingCompat.updateSplitAttributes(TEST_SPLIT_INFO, TEST_SPLIT_ATTRIBUTES)
-        verify(embeddingExtension)
-            .updateSplitAttributes(INVALID_SPLIT_INFO_TOKEN, OemSplitAttributes.Builder().build())
-
-        embeddingCompat.invalidateTopVisibleSplitAttributes()
+        embeddingCompat.invalidateVisibleActivityStacks()
         verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
+    }
+
+    @Test
+    fun testWindowExtensionsVersion4() {
+        testRule.overrideExtensionVersion(4)
+        createTestEmbeddingCompat()
+
+        val splitInfo =
+            SplitInfo(
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                SplitAttributes.Builder().build(),
+                binder = INVALID_SPLIT_INFO_TOKEN,
+            )
+
+        embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+        verify(embeddingExtension)
+            .setSplitAttributesCalculator(
+                any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+            )
+
+        embeddingCompat.clearSplitAttributesCalculator()
+        verify(embeddingExtension).clearSplitAttributesCalculator()
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.setLaunchingActivityStack(options, activityStack)
+        }
+        verify(options, never()).putBinder(any(), any())
+
+        assertThrows(UnsupportedOperationException::class.java) {
+            embeddingCompat.finishActivityStacks(emptySet())
+        }
+        verify(embeddingExtension, never())
+            .finishActivityStacksWithTokens(any<Set<ActivityStackToken>>())
+
+        embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
+        verify(embeddingExtension)
+            .updateSplitAttributes(splitInfo.getBinder(), OemSplitAttributes.Builder().build())
+
+        embeddingCompat.invalidateVisibleActivityStacks()
+        verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
+    }
+
+    @Test
+    fun testWindowExtensionsVersion5() {
+        testRule.overrideExtensionVersion(5)
+        createTestEmbeddingCompat()
+
+        val splitInfo =
+            SplitInfo(
+                ActivityStack(emptyList(), isEmpty = true),
+                ActivityStack(emptyList(), isEmpty = true),
+                SplitAttributes.Builder().build(),
+                token = SplitInfoToken.createFromBinder(INVALID_SPLIT_INFO_TOKEN),
+            )
+
+        embeddingCompat.setSplitAttributesCalculator { _ -> TEST_SPLIT_ATTRIBUTES }
+        verify(embeddingExtension)
+            .setSplitAttributesCalculator(
+                any<Function<OemSplitAttributesCalculatorParams, OemSplitAttributes>>()
+            )
+
+        embeddingCompat.clearSplitAttributesCalculator()
+        verify(embeddingExtension).clearSplitAttributesCalculator()
+
+        embeddingCompat.setLaunchingActivityStack(options, activityStack)
+        verify(options)
+            .putBundle(eq(ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN), any())
+
+        embeddingCompat.finishActivityStacks(emptySet())
+        verify(embeddingExtension).finishActivityStacksWithTokens(emptySet())
+
+        embeddingCompat.updateSplitAttributes(splitInfo, TEST_SPLIT_ATTRIBUTES)
+        verify(embeddingExtension)
+            .updateSplitAttributes(splitInfo.getToken(), OemSplitAttributes.Builder().build())
+
+        embeddingCompat.invalidateVisibleActivityStacks()
+        verify(embeddingExtension).invalidateTopVisibleSplitAttributes()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
+    }
+
+    @Test
+    fun testWindowExtensionsVersion6() {
+        testRule.overrideExtensionVersion(6)
+        createTestEmbeddingCompat()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
+    }
+
+    @Test
+    fun testWindowExtensionsVersion7() {
+        testRule.overrideExtensionVersion(7)
+        createTestEmbeddingCompat()
+
+        verifyOverlayFeatureApis()
+        verifyActivityWindowInfoCallbackController()
+    }
+
+    private fun verifyOverlayFeatureApis() {
+        if (WindowSdkExtensions.getInstance().extensionVersion >= OVERLAY_FEATURE_VERSION) {
+            embeddingCompat.setOverlayCreateParams(options, OverlayCreateParams())
+            // Verify if the overlay tag is put to the activityOptions bundle
+            verify(options).putString(any(), any())
+
+            val calculator = { _: OverlayAttributesCalculatorParams -> OverlayAttributes() }
+            embeddingCompat.setOverlayAttributesCalculator(calculator)
+            assertEquals(
+                calculator,
+                embeddingCompat.overlayController!!.overlayAttributesCalculator
+            )
+
+            embeddingCompat.updateOverlayAttributes("", OverlayAttributes())
+            verify(embeddingCompat.overlayController)!!.updateOverlayAttributes(
+                "",
+                OverlayAttributes()
+            )
+
+            val executor = mock<Executor>()
+            embeddingCompat.addOverlayInfoCallback("", executor) {}
+            verify(embeddingCompat.overlayController)!!.addOverlayInfoCallback(
+                eq(""),
+                eq(executor),
+                any()
+            )
+
+            embeddingCompat.removeOverlayInfoCallback {}
+            verify(embeddingCompat.overlayController)!!.removeOverlayInfoCallback(any())
+        } else {
+            assertThrows(UnsupportedOperationException::class.java) {
+                embeddingCompat.setOverlayCreateParams(options, OverlayCreateParams())
+            }
+            // Verify if the overlay tag is put to the activityOptions bundle
+            verify(options, never()).putString(any(), any())
+
+            assertThrows(UnsupportedOperationException::class.java) {
+                embeddingCompat.setOverlayAttributesCalculator { _ -> OverlayAttributes() }
+            }
+            assertNull(embeddingCompat.overlayController)
+
+            assertThrows(UnsupportedOperationException::class.java) {
+                embeddingCompat.updateOverlayAttributes("", OverlayAttributes())
+            }
+            verify(embeddingExtension, never()).updateActivityStackAttributes(any(), any())
+
+            embeddingCompat.addOverlayInfoCallback("", Runnable::run) {}
+            verify(embeddingExtension, never()).registerActivityStackCallback(any(), any())
+
+            embeddingCompat.removeOverlayInfoCallback {}
+            verify(embeddingExtension, never()).unregisterActivityStackCallback(any())
+        }
+    }
+
+    private fun verifyActivityWindowInfoCallbackController() {
+        if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
+            ActivityWindowInfoCallbackController(embeddingExtension)
+        } else {
+            assertThrows(UnsupportedOperationException::class.java) {
+                ActivityWindowInfoCallbackController(embeddingExtension)
+            }
+        }
+    }
+
+    private fun createTestEmbeddingCompat() {
+        val overlayController =
+            if (WindowSdkExtensions.getInstance().extensionVersion >= OVERLAY_FEATURE_VERSION) {
+                spy(
+                    OverlayControllerImpl(
+                        embeddingExtension,
+                        EmbeddingAdapter(PredicateAdapter(classLoader))
+                    )
+                )
+            } else {
+                null
+            }
+        overlayController?.apply {
+            doNothing().whenever(this).updateOverlayAttributes(any(), any())
+            doNothing().whenever(this).addOverlayInfoCallback(any(), any(), any())
+            doNothing().whenever(this).removeOverlayInfoCallback(any())
+        }
+
+        val activityWindowInfoCallbackController =
+            if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
+                spy(ActivityWindowInfoCallbackController(embeddingExtension))
+            } else {
+                null
+            }
+
+        embeddingCompat =
+            EmbeddingCompat(
+                embeddingExtension,
+                EmbeddingAdapter(PredicateAdapter(classLoader)),
+                ConsumerAdapter(classLoader),
+                applicationContext,
+                overlayController,
+                activityWindowInfoCallbackController,
+            )
     }
 
     companion object {
@@ -184,7 +417,6 @@
                 ActivityStack(emptyList(), isEmpty = true),
                 ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder().build(),
-                INVALID_SPLIT_INFO_TOKEN,
             )
 
         private val TEST_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
index 70fa3cf..43817d5 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesCalculatorParamsTest.kt
@@ -32,7 +32,7 @@
 
     @Test
     fun testSplitAttributesCalculatorParams() {
-        val parentWindowMetrics = WindowMetrics(Rect())
+        val parentWindowMetrics = WindowMetrics(Rect(), density = 1f)
         val parentConfiguration = Configuration()
         val parentWindowLayoutInfo = WindowLayoutInfo(emptyList())
         val defaultSplitAttributes = SplitAttributes.Builder().build()
@@ -59,7 +59,7 @@
 
     @Test
     fun testToString() {
-        val parentWindowMetrics = WindowMetrics(Rect())
+        val parentWindowMetrics = WindowMetrics(Rect(), density = 1f)
         val parentConfiguration = Configuration()
         val parentWindowLayoutInfo = WindowLayoutInfo(emptyList())
         val defaultSplitAttributes = SplitAttributes.Builder().build()
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
index 313fd4e..9eef488 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
@@ -16,7 +16,10 @@
 
 package androidx.window.embedding
 
+import android.graphics.Color
 import androidx.window.core.WindowStrictModeException
+import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
+import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
 import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
@@ -42,16 +45,38 @@
             SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_EQUAL)
                 .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         val attrs2 =
             SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_HINGE)
                 .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         val attrs3 =
             SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_HINGE)
                 .setLayoutDirection(TOP_TO_BOTTOM)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+                .build()
+        val animParams =
+            EmbeddingAnimationParams.Builder()
+                .setAnimationBackground(
+                    EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+                )
+                .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                .build()
+        val attrs4 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_HINGE)
+                .setLayoutDirection(TOP_TO_BOTTOM)
+                .setAnimationParams(animParams)
+                .build()
+        val attrs5 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_HINGE)
+                .setLayoutDirection(TOP_TO_BOTTOM)
+                .setAnimationParams(animParams)
                 .build()
 
         assertNotEquals(attrs1, attrs2)
@@ -62,6 +87,62 @@
 
         assertNotEquals(attrs3, attrs1)
         assertNotEquals(attrs3.hashCode(), attrs1.hashCode())
+
+        assertNotEquals(attrs3, attrs4)
+        assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
+
+        assertEquals(attrs4, attrs5)
+        assertEquals(attrs4.hashCode(), attrs5.hashCode())
+    }
+
+    @Test
+    fun testSplitAttributesEquals_withDividerAttributes() {
+        // No divider
+        val attrs1 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+                .build()
+
+        // Fixed divider
+        val attrs2 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+                .setDividerAttributes(FixedDividerAttributes.Builder().build())
+                .build()
+
+        // Draggable divider
+        val attrs3 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+                .setDividerAttributes(DraggableDividerAttributes.Builder().build())
+                .build()
+
+        // Draggable divider same as attrs3
+        val attrs4 =
+            SplitAttributes.Builder()
+                .setSplitType(SPLIT_TYPE_EQUAL)
+                .setLayoutDirection(LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
+                .setDividerAttributes(DraggableDividerAttributes.Builder().build())
+                .build()
+
+        // No divider vs fixed divider
+        assertNotEquals(attrs1, attrs2)
+        assertNotEquals(attrs1.hashCode(), attrs2.hashCode())
+
+        // Fixed divider vs draggable divider
+        assertNotEquals(attrs2, attrs3)
+        assertNotEquals(attrs2.hashCode(), attrs3.hashCode())
+
+        // Same draggable divider
+        assertEquals(attrs3, attrs4)
+        assertEquals(attrs3.hashCode(), attrs4.hashCode())
     }
 
     @Test
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
index 2636fce..cca8a13 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitControllerTest.kt
@@ -18,7 +18,6 @@
 
 import android.app.Activity
 import androidx.core.util.Consumer
-import androidx.window.core.ExperimentalWindowApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.flow.toList
@@ -34,7 +33,7 @@
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
-@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 internal class SplitControllerTest {
 
     private val mockBackend = mock<EmbeddingBackend>()
@@ -50,7 +49,6 @@
                         ActivityStack(emptyList(), true),
                         ActivityStack(emptyList(), true),
                         SplitAttributes(),
-                        mock()
                     )
                 )
             doAnswer { invocationOnMock ->
@@ -98,15 +96,8 @@
                 ActivityStack(emptyList(), true),
                 ActivityStack(emptyList(), true),
                 mockSplitAttributes,
-                mock()
             )
         splitController.updateSplitAttributes(mockSplitInfo, mockSplitAttributes)
         verify(mockBackend).updateSplitAttributes(eq(mockSplitInfo), eq(mockSplitAttributes))
     }
-
-    @Test
-    fun test_invalidateTopVisibleSplitAttributes_delegates() {
-        splitController.invalidateTopVisibleSplitAttributes()
-        verify(mockBackend).invalidateTopVisibleSplitAttributes()
-    }
 }
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
index 71bfc69..fd77ee8 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -18,14 +18,24 @@
 
 import android.app.Activity
 import android.os.Binder
+import androidx.window.WindowSdkExtensionsRule
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.mockito.kotlin.mock
 
 /** Unit tests for [SplitInfo] */
 class SplitInfoTest {
 
+    @get:Rule val testRule = WindowSdkExtensionsRule()
+
+    @Before
+    fun setUp() {
+        testRule.overrideExtensionVersion(3)
+    }
+
     @Test
     fun testSplitInfoContainsActivityFirstStack() {
         val activity = mock<Activity>()
@@ -95,6 +105,6 @@
 
     private fun createTestActivityStack(
         activitiesInProcess: List<Activity>,
-        isEmpty: Boolean = false,
+        isEmpty: Boolean = false
     ): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
 }
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
index 1fdfe34..35195af 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitPairRuleTest.kt
@@ -17,6 +17,7 @@
 package androidx.window.embedding
 
 import android.content.ComponentName
+import android.graphics.Color
 import android.graphics.Rect
 import androidx.window.core.ActivityComponentInfo
 import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
@@ -78,6 +79,7 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         TestCase.assertNull(rule.tag)
         assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
@@ -101,6 +103,14 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+                        )
+                        .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         filters.add(SplitPairFilter(ComponentName("a", "b"), ComponentName("c", "d"), "ACTION"))
         val rule =
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
index bd5dfe5..857a895 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitPlaceHolderRuleTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName
 import android.content.Intent
+import android.graphics.Color
 import android.graphics.Rect
 import androidx.window.core.ActivityComponentInfo
 import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
@@ -72,6 +73,7 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .setAnimationParams(EmbeddingAnimationParams.Builder().build())
                 .build()
         assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
         assertTrue(rule.checkParentBounds(density, validBounds))
@@ -91,6 +93,14 @@
             SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
+                .setAnimationParams(
+                    EmbeddingAnimationParams.Builder()
+                        .setAnimationBackground(
+                            EmbeddingAnimationBackground.createColorBackground(Color.GREEN)
+                        )
+                        .setCloseAnimation(EmbeddingAnimationParams.AnimationSpec.JUMP_CUT)
+                        .build()
+                )
                 .build()
         val rule =
             SplitPlaceholderRule.Builder(filters, intent)
diff --git a/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt b/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
index 1a51386..c34c857 100644
--- a/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
+++ b/window/window/src/test/java/androidx/window/reflection/ReflectionUtilsTest.kt
@@ -36,43 +36,46 @@
 
     @Test
     fun testValidateReflectionSuccess() {
-        val result = validateReflection { true }
+        val result = validateReflection("") { true }
         assertTrue(result)
     }
 
     @Test
     fun testValidateReflectionFail() {
-        val result = validateReflection {
-            classLoader.loadClass("SomeUnExistedClass.java")
-            true
-        }
+        val result =
+            validateReflection("") {
+                classLoader.loadClass("SomeUnExistedClass.java")
+                true
+            }
         assertFalse(result)
     }
 
     @Test
     fun testMethodModifier() {
-        val result = validateReflection {
-            val testClass = this::class.java
-            val privateMethod = testClass.getDeclaredMethod("testMethod").isPublic
-            assertFalse(privateMethod)
-            val publicMethod = testClass.getDeclaredMethod("testMethodModifier").isPublic
-            assertTrue(publicMethod)
-            true
-        }
+        val result =
+            validateReflection("") {
+                val testClass = this::class.java
+                val privateMethod = testClass.getDeclaredMethod("testMethod").isPublic
+                assertFalse(privateMethod)
+                val publicMethod = testClass.getDeclaredMethod("testMethodModifier").isPublic
+                assertTrue(publicMethod)
+                true
+            }
         assertTrue(result)
     }
 
     @Test
     fun testDoesReturn() {
-        val result = validateReflection {
-            val testClass = this::class.java
-            val privateMethod = testClass.getDeclaredMethod("testMethod")
-            assertTrue(privateMethod.doesReturn(Int::class.java))
-            assertTrue(privateMethod.doesReturn(Int::class))
-            assertFalse(privateMethod.doesReturn(Any::class.java))
-            assertFalse(privateMethod.doesReturn(Any::class))
-            true
-        }
+        val result =
+            validateReflection("") {
+                val testClass = this::class.java
+                val privateMethod = testClass.getDeclaredMethod("testMethod")
+                assertTrue(privateMethod.doesReturn(Int::class.java))
+                assertTrue(privateMethod.doesReturn(Int::class))
+                assertFalse(privateMethod.doesReturn(Any::class.java))
+                assertFalse(privateMethod.doesReturn(Any::class))
+                true
+            }
         assertTrue(result)
     }
 
diff --git a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
index a0c2a4b..dffdc4f 100644
--- a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
+++ b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
@@ -66,7 +66,7 @@
 
     override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
         val bounds = overrideBounds ?: currentBounds[context] ?: Rect()
-        return WindowMetrics(bounds)
+        return WindowMetrics(bounds, density = 1f)
     }
 
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
@@ -75,7 +75,7 @@
 
     override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
         val bounds = overrideMaxBounds ?: maxBounds[context] ?: Rect()
-        return WindowMetrics(bounds)
+        return WindowMetrics(bounds, density = 1f)
     }
 
     /** Clears any overrides set with [.setCurrentBounds] or [.setCurrentBoundsForActivity]. */
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 7bac479..e9e5895 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -26,6 +26,7 @@
             minifyEnabled = true
         }
     }
+    compileSdkVersion 35
     defaultConfig {
         javaCompileOptions {
             annotationProcessorOptions {
diff --git a/work/work-datatransfer/build.gradle b/work/work-datatransfer/build.gradle
index b90d5b32..21e3a10 100644
--- a/work/work-datatransfer/build.gradle
+++ b/work/work-datatransfer/build.gradle
@@ -30,6 +30,7 @@
 }
 
 android {
+    compileSdk 35
     defaultConfig {
         minSdkVersion 23
     }
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index a6faef5..ef6a81f0 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -30,6 +30,7 @@
 }
 
 android {
+    compileSdk 35
     buildTypes.configureEach {
         consumerProguardFiles "proguard-rules.pro"
     }
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index 3b4bedfe..9b96902 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -30,6 +30,7 @@
 }
 
 android {
+    compileSdk 35
     buildTypes.configureEach {
         consumerProguardFiles "proguard-rules.pro"
     }
diff --git a/work/work-runtime-ktx/build.gradle b/work/work-runtime-ktx/build.gradle
index e7a7ef4..db3a25c 100644
--- a/work/work-runtime-ktx/build.gradle
+++ b/work/work-runtime-ktx/build.gradle
@@ -43,5 +43,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.work.ktx"
 }
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index f4f14b5..99dd6e5 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -55,6 +55,7 @@
             }
         }
     }
+    compileSdk = 35
     sourceSets {
         androidTest.assets.srcDirs += files("$projectDir/src/schemas".toString())
     }
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 8dd4953..6397ca8 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -401,6 +401,20 @@
         }
     }
 
+    @Test
+    @SmallTest
+    public void testEnsureTraceTags() {
+        if (Build.VERSION.SDK_INT < 35) {
+            return;
+        }
+
+        final String id = "id";
+        WorkSpec workSpec = new WorkSpec(id, TestWorker.class.getName());
+        workSpec.setTraceTag(TestWorker.class.getSimpleName());
+        JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+        assertEquals(jobInfo.getTraceTag(), TestWorker.class.getSimpleName());
+    }
+
     private WorkSpec getTestWorkSpecWithConstraints(Constraints constraints) {
         return new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setConstraints(constraints)
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
index f21c8f1..ae791e3 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
@@ -51,6 +51,7 @@
 import java.util.concurrent.CancellationException
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.Future
+import kotlin.collections.removeLast as removeLastKt
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.CancellableContinuation
@@ -420,7 +421,7 @@
     private fun iterativelyFailWorkAndDependents(workSpecId: String) {
         val idsToProcess = mutableListOf(workSpecId)
         while (idsToProcess.isNotEmpty()) {
-            val id = idsToProcess.removeLast()
+            val id = idsToProcess.removeLastKt()
             // Don't fail already cancelled work.
             if (workSpecDao.getState(id) !== WorkInfo.State.CANCELLED) {
                 workSpecDao.setState(WorkInfo.State.FAILED, id)
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index dc462d6..158f9fc 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -139,6 +139,13 @@
             //noinspection NewApi
             builder.setExpedited(true);
         }
+        if (Build.VERSION.SDK_INT >= 35) {
+            // Add a trace tag that shows the actual worker running.
+            String traceTag = workSpec.getTraceTag();
+            if (traceTag != null) {
+                builder.setTraceTag(traceTag);
+            }
+        }
         return builder.build();
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
index 4854c3d..363724e 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.kt
@@ -25,6 +25,7 @@
 import androidx.work.impl.WorkManagerImpl
 import androidx.work.launchOperation
 import java.util.UUID
+import kotlin.collections.removeLast as removeLastKt
 
 private fun cancel(workManagerImpl: WorkManagerImpl, workSpecId: String) {
     iterativelyCancelWorkAndDependents(workManagerImpl.workDatabase, workSpecId)
@@ -48,7 +49,7 @@
     val dependencyDao = workDatabase.dependencyDao()
     val idsToProcess = mutableListOf(workSpecId)
     while (idsToProcess.isNotEmpty()) {
-        val id = idsToProcess.removeLast()
+        val id = idsToProcess.removeLastKt()
         // Don't fail already cancelled work.
         val state = workSpecDao.getState(id)
         if (state !== WorkInfo.State.SUCCEEDED && state !== WorkInfo.State.FAILED) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
index 47d8d5a..9e35c48 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
@@ -30,6 +30,7 @@
 import androidx.work.impl.model.WorkSpec
 import androidx.work.impl.workers.ARGUMENT_CLASS_NAME
 import androidx.work.impl.workers.ConstraintTrackingWorker
+import kotlin.collections.removeLast as removeLastKt
 
 internal fun checkContentUriTriggerWorkerLimits(
     workDatabase: WorkDatabase,
@@ -40,7 +41,7 @@
     val continuations = mutableListOf(continuation)
     var newCount = 0
     while (continuations.isNotEmpty()) {
-        val current = continuations.removeLast()
+        val current = continuations.removeLastKt()
         newCount += current.work.count { it.workSpec.constraints.hasContentUriTriggers() }
         (current.parents as List<WorkContinuationImpl>?)?.let { continuations.addAll(it) }
     }
diff --git a/work/work-rxjava2/build.gradle b/work/work-rxjava2/build.gradle
index 0a17f15..98670f7 100644
--- a/work/work-rxjava2/build.gradle
+++ b/work/work-rxjava2/build.gradle
@@ -50,5 +50,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.work.rxjava2"
 }
diff --git a/work/work-rxjava3/build.gradle b/work/work-rxjava3/build.gradle
index 8457e44..f7aba04 100644
--- a/work/work-rxjava3/build.gradle
+++ b/work/work-rxjava3/build.gradle
@@ -49,5 +49,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.work.rxjava3"
 }
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index 32597de..486b03a 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -63,5 +63,6 @@
 }
 
 android {
+    compileSdk 35
     namespace "androidx.work.testing"
 }